React版本:15.4.2
**翻译:xiyoki **
通常几个组件需要响应相同的数据变化。我们建议将共享状态提升到最接近的共同祖先。让我们看看这是如何工作的。
在本节中,我们将创建一个温度计算器,用于计算水是否在给定温度下沸腾。
我们将从名为BoilingVerdict
的组件开始。它接受celsius温度为props,并打印是否足以将水烧开:
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.value
中。
此外,它用当前值渲染BoilingVerdict
。
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {value: ''};
}
handleChange(e) {
this.setState({value: e.target.value});
}
render() {
const value = this.state.value;
return (
<fieldset>
<legend>Enter temperature in Celsius:</legend>
<input
value={value}
onChange={this.handleChange} />
<BoilingVerdict
celsius={parseFloat(value)} />
</fieldset>
);
}
}
Adding a Second Input(增加第二个输入)
我们新的要求是:除了摄氏度输入框,我们还提供华氏度输入框,并且二者是同步的。
我们把从Calculator
中提取一个TemperatureInput
组件作为开始。我们将为它增加一个新scale prop,并且这个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 = {value: ''};
}
handleChange(e) {
this.setState({value: e.target.value});
}
render() {
const value = this.state.value;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={value}
onChange={this.handleChange} />
</fieldset>
);
}
}
现在,我们可以改变Calculator
来渲染两个独立的温度输入:
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
现在我们有两个输入框,但当你向其中一个输入框输入温度时,另一个并不会更新。这违反了我们的要求:我们希望它们保持同步。
我们也不能从Calculator
中展示BoilingVerdict
。Calculator
也不知道当前的温度,因为当前温度被隐藏在了TemperatureInput
内部。
Lifting State Up(状态提升)
首先,我们将写两个函数来将摄氏度转换为华氏度,将华氏度转换为摄氏度:
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
这两个函数转换数字。我们将写另一个函数,它接受一个字符串value和一个转换器函数作为参数,并返回一个字符串。我们将使用它来计算其中一个输入框的值,而该输入框的值基于另一个输入框。
它返回一个无效的空字符串value,并且它将输出四舍五入到三位小数:
function tryConvert(value, convert) {
const input = parseFloat(value);
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
中删除状态。
相反,TemperatureInput
组件将接受value
和onChange
处理程序作为prop:
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onChange(e.target.value);
}
render() {
const value = this.props.value;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={value}
onChange={this.handleChange} />
</fieldset>
);
}
}
如果几个组件需要访问相同的状态,这标志着状态应该被提升到最接近的共同祖先。在这个例子中最接近的祖先就是Calculator
。我们将在它的状态中存储当前的value
和scale
。
我们可以存储两个输入的值,但事实证明这是不必要的。它足以存储最近被更改的输入框的值,以及其表示的scale
。然后我们能基于当前的value
和scale
,单独推断其他输入框的值。
输入的值保存同步,因为它们的值从相同的状态计算而来。
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = {value: '', scale: 'c'};
}
handleCelsiusChange(value) {
this.setState({scale: 'c', value});
}
handleFahrenheitChange(value) {
this.setState({scale: 'f', value});
}
render() {
const scale = this.state.scale;
const value = this.state.value;
const celsius = scale === 'f' ? tryConvert(value, toCelsius) : value; {/* 对状态value作进一步处理*/}
const fahrenheit = scale === 'c' ? tryConvert(value, toFahrenheit) : value;
return (
<div>
<TemperatureInput
scale="c"
value={celsius}
onChange={this.handleCelsiusChange} />
<TemperatureInput
scale="f"
value={fahrenheit}
onChange={this.handleFahrenheitChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</div>
);
}
}
不论你编辑哪个输入框,Calculator
中的this.state.value
和this.state.scale
都会获得更新。其中一个输入框获取的值为原样,因此任何用户的输入都被保留,另一个输入框中的值总是基于它重新计算。
Lessons Learned (得到的教训)
对于在React应用程序中更改的任何数据,应该有一个单一的‘真实来源’。通常,首先将状态添加到需要渲染的组件。然后,如果其他组件也需要它,你可以将其提升到最接近的共同祖先。而不是尝试在不同组件之间同步状态,你应该依赖于自上而下的数据流。
提升状态涉及编写比双向绑定方法更多的‘样板’代码。但好处是找到和隔离bug需要较少的工作。由于任何状态存在于特定的组件中,并且该组件可以单独改变它,所以大大减少了错误的表面积。此外,你可以实现任何自定义逻辑以拒绝或转换用户输入。
如果数据可以从props或state派生,那么它就不应该在状态之中。例如,我们只存储了最后编辑的value
和scale
,而不是存储两个celsiusValue
和fahrenheitValue
。另一个输入的值总是可以从render()
方法中计算出来。这允许我们清除或应用四舍五入到其他字段,而不会丢失用户输入的任何精度。
当你在UI中看到错误时,可以使用 React Developer Tools 检查props,并向上移动树,直到找到负责更新状态的组件。这使你可以跟踪错误到其来源: