# 提升状态
通常,一些组件需要反映相同的修改数据。我们建议提升共享的状态到它们最近的共同祖先组件。我们看下这是如何运作的。
在本节,我们将创建一个温度计算器,计算水在一个给定温度是否会沸腾。
我们通过一个称为 BoilingVerdict 的组件开始。它接受摄氏温度作为 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.value 中保存它的值。
另外,它对当前的输入值渲染 BoiliingVerdict 。
~~~
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>
);
}
}
~~~
在 CodePen 中[打开查看](http://codepen.io/gaearon/pen/Gjxgrj?editors=0010)。
## 添加第二个 input
我们新的需求是,除了输入一个摄氏温度,我们还提供了一个华氏温度输入,它们保持同步。
我们可以从 Calculator 中提取一个 TemperatureInput 组件开始。我们将添加一个新的 scale 属性,值可能是 “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>
);
}
}
~~~
在 CodePen 中[打开查看](http://codepen.io/gaearon/pen/NRrzOL?editors=0010)。
我们现在有两个 input 了,但是当你输入了其中一个温度,其它的并没有更新。这是跟我们的需要不符的:我们希望保持它们同步。
我们也不能从 Calculator 中显示 BoilingVerdict 了。Calculator 不知道当前的温度,因为它是在 TemperatureInput 中隐藏的。
## 提升状态
首先,我们编写两个函数来在摄氏温度和华氏温度之间转换:
~~~
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
~~~
这两个函数可以转换数值。再编写另一个函数接受一个字符串值和一个转换函数作为参数,返回一个字符串。我们使用它进行基于其中一个输入对另一个的计算。
对于无效的输入值,它返回一个空字符串,它保留第三个小数位位置:
~~~
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 中的 state 。
相反,它通过 props 接受 value 和 onChange 处理程序:
~~~
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>
);
}
}
~~~
如果一些组件需要访问相同的状态,这是一个信号说明 state 应该被提升状态到它们最近的祖先了。在这里,这个组件是 Calculator。我们将保存当前的 value 和 scale 到它的 state。
我们将保存 input 的 value ,但是它结果是不必要的。保存 value 和它表示的 scale 到最新修改的 input 就足够了。然后我们可以根据当前的 value 和 scale 推断出另一个 input 的 value。
input 会保持同步因为它们的值从同一个 state 计算而来:
~~~
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;
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>
);
}
}
~~~
在 CodePen 中[打开查看](http://codepen.io/gaearon/pen/ozdyNg?editors=0010)。
现在,无论你编辑哪个 input ,Calculator 中的 this.state.value 和 this.state.scale 都会得到更新。一个输入框获得值,那么任何用户输入都会被保存,另一个输入框的值也总是基于它进行重新计算。
## 经验总结
在 一个 React 应用中,对于任何修改的数据应该有一个单独的“真实源”。通常,state 首先被添加到需要它进行渲染的组件。然后,如果其它的组件也需要它,你可以提升状态到它们最近的祖先。你应该依赖[从上到下的数据流向](https://facebook.github.io/react/docs/state-and-lifecycle.html#the-data-flows-down),而不是试图在不同的组件中同步状态。
提升状态涉及到比编写两种方式的绑定策略更“样板化”的代码,但是有一个好处,它可以更方便的找出并远离 bugs。由于任何状态都生存在一些组件中,而且组件单独的修改它,发生错误的可能大大减少。另外,你可以实现任何定制的逻辑来拒绝或者转换用户输入。
如果可以从 props 或者 state 得来,它可能不是在 state 中。例如,我们只保存最后编辑的 value 和它的 sacle,而不是保存 celsiusValue 和 fahrenheitValue 。另外的输入框总是在 render() 方法中计算得来。这使我们清除或者应用舍入其它字段而不会丢失任何用户输入的精度。
当你看到 UI 中的错误,你可以使用 [React 开发者工具](https://github.com/facebook/react-devtools)来检查 props 并遍历树,直到找出负责更新状态的组件。这使你可以跟踪到 bug 的源头:
![](https://box.kancloud.cn/83cba3be536a9fb668e7a4eebd32544a_640x334.gif)