通常,几个组件需要根据同一个数据变化做出响应。我们建议将这个共享的状态提升到他们最近的一个共用祖先。让我们看看实际该怎么做。
在这一节,我们将创建一个温度计算器,用来计算一给定温度能否让水沸腾。
我们从名为BoilingVerdict
的组件开始。它接受celsius
温度作为prop,然后打印出是否足够使水沸腾:
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>The water would boil.</p>;
}
return <p>The water would not boil.</p>;
}
接下来,我们创建一个Calculator
组件。它渲染一个供你输入温度的<input>
,并将它的值存在this.state.temperature
中。
除此之外,他还为当前的输入渲染一个BoilingVerdict
。
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
handleChange(e) {
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
return (
<fieldset>
<legend>Enter temperature in Celsius:</legend>
<input
value={temperature}
onChange={this.handleChange} />
<BoilingVerdict
celsius={parseFloat(temperature)} />
</fieldset>
);
}
}
添加第二个输入
我们有个新的需求,除了一个摄氏度输入,我们还要提供一个华氏输入,并且他们保持同步。
我们先从Calculator
中提取TemperatureInput
组件。然后为其添加一个新的scale
prop,它的值值为"c"
或"f"
:
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
};
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
handleChange(e) {
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={temperature}
onChange={this.handleChange} />
</fieldset>
);
}
}
现在我们可以改变Calculator
来渲染两个独立的温度输入:
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
在CodePen上试一试
现在我们有两个输入了,但是当你在其中一个里输入温度,另一个不会去更新。这就不满足我们的需求了,我们想让他们同步。
我们也没在Calculator
中显示BoilingVerdict
。Calculator
不知道当前的温度,因为温度被隐藏在TemperatureInput
中。
编写转换函数
首先我们写两个函数来互相转换摄氏度和华氏温度。
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
这两个函数转换数字。接下来我们写另一个函数,接受一个temperature
字符串和一个转换函数作为参数,返回一个字符串。我们将用他来根据另一个input来计算这个input的值。
一个无效temperature
会使它返回一个空字符串,另外它会保证输出结果四舍五入到小数点后三位:
function tryConvert(temperature, convert) {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
}
比如,tryConvert('abc', toCelsius)
返回空字符串,tryConvert('10.22', toFahrenheit)
返回'50.396'
。
提升状态
目前,两个TemperatureInput
组件都单独地在本地状态中保存自己的值:
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
handleChange(e) {
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
然而,我们希望这两个input可以彼此同步。当我们更新摄氏度input,华氏度input也会相应的转换温度,反之亦然。
在React中,想要共享状态,就需要找到共享状态组件的一个最近共有祖先,然后通过将该状态移动到这个共有祖先上来完成共享。这称为“提升状态”。我们先移除TemperatureInput
的本地状态,取而代之的是将它移到Calculator
中。
如果Calculator
拥有共享状态,对于两个input中的温度来说他就成为了“真相的来源”。他就可以指示两个input具有相同的值。由于两个TemperatureInput
组件的props都来自同一个父组件Calculator
,两个input将始终保持同步。
让我们逐步分析这是如何工作的。
首先,在TemperatureInput
组件中,我们使用this.props.temperature
将this.state.temperature
替换掉。现在,让我们假设this.props.temperature
已经存在,之后我们会从Calculator
中传入该值:
render() {
// Before: const temperature = this.state.temperature;
const temperature = this.props.temperature;
我们知道props是只读的。之前temperature
在本地状态中,TemperatureInput
只能调用this.setState()
来改变它。而现在,temperature
作为prop从父组件获取,TemperatureInput
不能再控制它了。
在React中,一般通过将组件变为“受控”,来解决这个。就像DOM<input>
接受一个value
和一个onChange
prop,所以自定义的TemperatureInput
可以从它的父组件Calculator
中获取temperature
和onTemperatureChange
props。
现在,当TemperatureInput
想要更新它的温度值,调用this.props.onTemperatureChange
就好了:
handleChange(e) {
// Before: this.setState({temperature: e.target.value});
this.props.onTemperatureChange(e.target.value);
注意,自定义组件中的prop名字:temperature
和onTemperatureChange
并没什么特别的意思。我们可以随意命名,比如给它们更通用的名字value
和onChange
。
父组件Calculator
提供proponTemperatureChange
的同时也提供temperature
。他通过修改自己的本地状态来处理更改,从而将两个input重新渲染为新的值。我们马上就来看看新的Calculator
实现。
在深入Calculator
的变化之前,我们来重新看遍TemperatureInput
组件中做过什么变动。我们将他的本地状态移除,将从this.state.temperature
读取,改为从this.props.temperature
读取。当我们想做出变化时,现在我们调用Calculator
提供的this.props.onTemperatureChange()
,来代替之前的this.setState()
。
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onTemperatureChange(e.target.value);
}
render() {
const temperature = this.props.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={temperature}
onChange={this.handleChange} />
</fieldset>
);
}
}
现在让我们回到Calculator
组件。
我们将当前输入的temperature
和scale
存到他的本地状态。这就是我们从input中“提升”的状态,该状态将作为“真相的来源”提供给两个TemperatureInput
。这是我们渲染两个input,所需数据的最少表示。
假如,我们在摄氏度输入框中输入37,Calculator
组件的状态如下:
{
temperature: '37',
scale: 'c'
}
如果我们将华氏字段编辑为212,Calculator
的状态将变为:
{
temperature: '212',
scale: 'f'
}
我们可以存储两个输入的值,但实际上是不必的。存储最后一次变化的值和单位即可。然后我们可以根据当前的温度和单位来换算出另一个值。
因为两个input的值从同一个状态计算而来,所以他们始终保持同步:
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = {temperature: '', scale: 'c'};
}
handleCelsiusChange(temperature) {
this.setState({scale: 'c', temperature});
}
handleFahrenheitChange(temperature) {
this.setState({scale: 'f', temperature});
}
render() {
const scale = this.state.scale;
const temperature = this.state.temperature;
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={this.handleCelsiusChange} />
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={this.handleFahrenheitChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</div>
);
}
}
在CodePen上试一试
现在,不论你编辑哪一个input,Calculator
中的this.state.temperature
和this.state.scale
都会得到更新。从任何一个input获取的用户输入都会被保存,另一个input的值会根据它重新计算。
让我们重新看下当你编辑一个input时发生了什么:
- React调用DOM
<input>
上指定为onChange
的函数。在我们的例子中这个函数是TemperatureInput
组件的handleChange
方法。 -
TemperatureInput
中的handleChange
方法,使用新的需求值调用this.props.onTemperatureChange()
。他的props,包括onTemperatureChange
,都是由他的父组件Calculator
提供。 - 在渲染之前,
Calculator
已经将自己的handleCelsiusChange
方法赋值给摄氏度TemperatureInput
的onTemperatureChange
,还将自己的handleFaherenheitChange
方法赋值给华氏度TemperatureInput
的onTemperatureChange
。所以,Calculator
的这个两个方法会根据我们编辑的input,而得到调用。 - 在这些方法中,
Calculator
组件通过使用新的输入值和在编辑中input的单位来调用this.setState()
,使得React重新渲染自己。 - React通过调用
Calculator
组件的render
方法来获取UI的外观。两个input的值根据当前的温度和单位重新计算。温度的换算在这个时候进行。 - React根据
Calculator
提供的新props来分别调用TemperatureInput
的render
方法。由此得知他们UI的外观。 - React DOM更新DOM来匹配所需的input值。我们编辑的input接受当前的值,另一个input更新为转换后的问题。
每次更新都会重复上面的步骤,从而使所有input保持同步。
经验教训
在React应用中,所有变化的数据都应该是单独的“真相来源”。通常,状态第一个被添加到组件中(组件需要用这些状态来进行渲染)。如果其他组件也需要它,你可以将状态提升到这些组件共用的最近祖先。你应该依赖自上而下的数据流,而不是同步多个组件的状态。
比起双向绑定的方法,提升状态需要写更多的“样板”代码,但好处就是,它可以更轻松的找到和隔离bug。因为任何状态都是存在于组件中,并且只有组件可以修改它,由此bugs存在的区域大大被减少。另外,你可以实现任意逻辑来拒绝或转换用户的输入。
如果某个值可以通过其他props或状态获得,那他就不该把他放在状态中。比如,我们仅仅存储上一次编辑的温度和单位,而不是将celsiusValue
和fahrenheitValue
都存下来。因为在render()
方法中,一个input的值始终可以通过另一个计算得来。这样,我们对另一个字段用或不用四舍五入,都不会在用户的输入中丢失精度。
当你发现UI上有错误发生,你可以使用React开发者工具来检查props,然后沿着树结构向上,知道你找到负责更新状态的组件。这使你追溯到bug的来源: