状态提升
通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。子组件使用props获取数据,父组件使用state修改数据
例子
创建一个用于计算水在给定温度下是否会沸腾的温度计算器
目标
希望两个输入框内的数值彼此能够同步。当我们更新摄氏度输入框内的数值时,华氏度输入框内应当显示转换后的华氏温度,反之亦然。
在 React 中,将多个组件中需要共享的 state 向上移动到它们的最近共同父组件中,便可实现共享 state。这就是所谓的“状态提升”。接下来,我们将 TemperatureInput
组件中的 state 移动至 Calculator
组件中去。
如果 Calculator
组件拥有了共享的 state,它将成为两个温度输入框中当前温度的“数据源”。它能够使得两个温度输入框的数值彼此保持一致。由于两个 TemperatureInput
组件的 props 均来自共同的父组件 Calculator
,因此两个输入框中的内容将始终保持一致。
我们知道props是只读的。当 temperature
存在于 TemperatureInput
组件的 state 中时,组件调用 this.setState()
便可修改它。然而,temperature
是由父组件传入的 prop,TemperatureInput
组件便失去了对它的控制权。
在 React 中,这个问题通常是通过使用“受控组件”来解决的。与 DOM 中的 <input>
接受 value
和 onChange
一样,自定义的 TemperatureInput
组件接受 temperature
和 onTemperatureChange
这两个来自父组件 Calculator
的 props。
当 TemperatureInput
组件想更新温度时,需调用 this.props.onTemperatureChange
来更新它
onTemperatureChange
的 prop 和 temperature
的 prop 一样,均由父组件 Calculator
提供。它通过修改父组件自身的内部 state 来处理数据的变化,进而使用新的数值重新渲染两个输入框。
完整代码
// 打印出该温度是否足以将水煮沸的结果
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>The water would boil.</p>;
}
return <p>The water would not boil.</p>;
}
// 编写转换函数
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
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();
}
// 判断是什么类型的摄氏度
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
};
//Calculator 组件中抽离出 TemperatureInput 组件
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
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>
);
}
}
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>
);
}
}
ReactDOM.render(
<Calculator />,
document.getElementById('root')
);
步骤解析
- React 会调用 DOM 中
<input>
的onChange
方法。在本实例中,它是TemperatureInput
组件的handleChange
方法。 TemperatureInput
组件中的handleChange
方法会调用this.props.onTemperatureChange()
,并传入新输入的值作为参数。其 props 诸如onTemperatureChange
之类,均由父组件Calculator
提供。- 起初渲染时,用于摄氏度输入的子组件
TemperatureInput
中的onTemperatureChange
方法与Calculator
组件中的handleCelsiusChange
方法相同,而,用于华氏度输入的子组件TemperatureInput
中的onTemperatureChange
方法与Calculator
组件中的handleFahrenheitChange
方法相同。因此,无论哪个输入框被编辑都会调用Calculator
组件中对应的方法。 - 在这些方法内部,
Calculator
组件通过使用新的输入值与当前输入框对应的温度计量单位来调用this.setState()
进而请求 React 重新渲染自己本身。 - React 调用
Calculator
组件的render
方法得到组件的 UI 呈现。温度转换在这时进行,两个输入框中的数值通过当前输入温度和其计量单位来重新计算获得。 - React 使用
Calculator
组件提供的新 props 分别调用两个TemperatureInput
子组件的render
方法来获取子组件的 UI 呈现。 - React 调用
BoilingVerdict
组件的render
方法,并将摄氏温度值以组件 props 方式传入。 - React DOM 根据输入值匹配水是否沸腾,并将结果更新至 DOM。我们刚刚编辑的输入框接收其当前值,另一个输入框内容更新为转换后的温度值。
得益于每次的更新都经历相同的步骤,两个输入框的内容才能始终保持同步。
总结
在 React 应用中,任何可变数据应当只有一个相对应的唯一“数据源”。通常,state 都是首先添加到需要渲染数据的组件中去。然后,如果其他组件也需要这个 state,那么你可以将它提升至这些组件的最近共同父组件中。你应当依靠自上而下的数据流,而不是尝试在不同组件间同步 state。
虽然提升 state 方式比双向绑定方式需要编写更多的“样板”代码,但带来的好处是,排查和隔离 bug 所需的工作量将会变少。由于“存在”于组件中的任何 state,仅有组件自己能够修改它,因此 bug 的排查范围被大大缩减了。此外,你也可以使用自定义逻辑来拒绝或转换用户的输入。