通常情况下,许多组件需要共同反应数据变化。我们推荐将共享的状态提升到距离他们最近的共同父控件上。让我们看看这样是怎么工作的。

这一节中,我们将创造一个温度计算器根据给定温度来计算水是否沸腾。

我们开始于组件(沸腾裁决)。这个组件接受一个Celsius(摄氏)温度作为prop参数,打印它是否足够使得水烧开:

  1. function BoilingVerdict(props) {
  2. if (props.celsius >= 100) {
  3. return <p>The water would boil.</p>;
  4. }
  5. return <p>The water would not boil.</p>;
  6. }

下面,我们创建一个组件叫Calculator,它渲染一个Input元素来使用户键入温度,该组件将输入值储存在this.state.temperature。

另外,它也根据当前输入值渲染了BoilingVerdict。

  1. class Calculator extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.handleChange = this.handleChange.bind(this);
  5. this.state = {temperature: ''};
  6. }
  7. handleChange(e) {
  8. this.setState({temperature: e.target.value});
  9. }
  10. render() {
  11. const temperature = this.state.temperature;
  12. return (
  13. <fieldset>
  14. <legend>Enter temperature in Celsius:</legend>
  15. <input
  16. value={temperature}
  17. onChange={this.handleChange} />
  18. <BoilingVerdict
  19. celsius={parseFloat(temperature)} />
  20. </fieldset>
  21. );
  22. }
  23. }

添加第二个Input

我们的需求是:在摄氏输入的基础上,添加Fahrenheit华氏输入,并且使他们保持同步。

我们以从Calculator中提取TemperatureInput组件作为开始。我们需要添加新的度量单位,它们可以是‘c’或‘f’。

  1. const scaleNames = {
  2. c: 'Celsius',
  3. f: 'Fahrenheit'
  4. };
  5. class TemperatureInput extends React.Component {
  6. constructor(props) {
  7. super(props);
  8. this.handleChange = this.handleChange.bind(this);
  9. this.state = {temperature: ''};
  10. }
  11. handleChange(e) {
  12. this.setState({temperature: e.target.value});
  13. }
  14. render() {
  15. const temperature = this.state.temperature;
  16. const scale = this.props.scale;
  17. return (
  18. <fieldset>
  19. <legend>Enter temperature in {scaleNames[scale]}:</legend>
  20. <input value={temperature}
  21. onChange={this.handleChange} />
  22. </fieldset>
  23. );
  24. }
  25. }

现在我们可以把Calculator组件变成渲染两个独立的温度输入组件:

  1. class Calculator extends React.Component {
  2. render() {
  3. return (
  4. <div>
  5. <TemperatureInput scale="c" />
  6. <TemperatureInput scale="f" />
  7. </div>
  8. );
  9. }
  10. }

我们现在有两个input输入框,但是当你在他们任何一个中输入温度,另外一个不会随之更新。这点和我们的需求相违背:我们想使他们保持同步。

同样我们不能在Calculator中显示BoilingVerdict。Calculator并不知道当前温度因为BoilingVerdict不见了。

书写转换的函数

首先,我们写两个函数是的Celsius和Fahrenheit之间得以转换:

  1. function toCelsius(fahrenheit) {
  2. return (fahrenheit - 32) * 5 / 9;
  3. }
  4. function toFahrenheit(celsius) {
  5. return (celsius * 9 / 5) + 32;
  6. }

这两个函数能转换数值。我们要写另外一个函数,接受一个String类型的temperature参数和一个转换函数作为参数,并返回String。我们将使用它根据其中一个input输入来计算另外的input的值。

当不可用的温度值传入,函数将输出空字符串,并且,它保持了小数点后三位的输出:

  1. function tryConvert(temperature, convert) {
  2. const input = parseFloat(temperature);
  3. if (Number.isNaN(input)) {
  4. return '';
  5. }
  6. const output = convert(input);
  7. const rounded = Math.round(output * 1000) / 1000;
  8. return rounded.toString();
  9. }

举例子来说:tryConvert(‘abc’,toCelsius)返回空字符串,tryConvert(‘10.22’, toFahrenheit)返回 ‘50.396’。

状态提升

现在,两个TemperatureInput都在他们自己的状态中维护他们的值:

  1. class TemperatureInput extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.handleChange = this.handleChange.bind(this);
  5. this.state = {temperature: ''};
  6. }
  7. handleChange(e) {
  8. this.setState({temperature: e.target.value});
  9. }
  10. render() {
  11. const temperature = this.state.temperature;
  12. // ...

但我们想使他们彼此同步。当我们更新摄氏输入框,华氏输入框也要呈现出相应的转换后的华氏温度,反之亦然。

在React中,共享状态是通过把某状态放到需要共享状态的组件们的最近共同祖先组件那里来实现的。这叫‘状态提升’。我们将把TemperatureInput的状态从组件中放到Calculator中。

如果Calcutator组件拥有了共享状态,那它对于两个温度输入组件来说就是“真理之源”。它能指示两个输入的值相互保持一致性。由于所有的TemperatureInput组件的props都来自相同的父组件Calculator组件,所以两个温度输入组件会同步。

让我们一步一步看看到底是怎么工作的:

首先,在TemperatureInput组件中把this.state.temperature用this.props.temperature代替。到目前为止,尽管我们之后要从Calculator中传递this.props.temperature,但让我们先假设它是存在的。

  1. render() {
  2. // Before: const temperature = this.state.temperature;
  3. const temperature = this.props.temperature;
  4. // ...

要知道,props是只读属性。当temperature在TemperatureInput组件里面时,TemperatureInput组件可以通过调用this.setState()改变temperature。但是现在temperature来自于父组件传递的prop属性,这样,TemperatureInput组件就是去了对temperature的控制。

在React中,通常解决这个问题是吧组建”控制”起来。就像原生DOM的input似的,它可以接受value和onChange属性,所以自定义的TemperatureInput组件也能从其父组件Calculator中接受temperature和onTemperatureChanged属性。

现在,TemperatureInput组件想要更新它的temperature值时,它会调用this.props.onTemperatureChange:

  1. handleChange(e) {
  2. // Before: this.setState({temperature: e.target.value});
  3. this.props.onTemperatureChange(e.target.value);
  4. // ...

注意: 在自定义组件里是叫temperature还是onTemperatureChanged这样的props名字没啥特别含义。我们可以起任何名字,像是通用的习惯value或onChange等。

父组件Calculator将会一并提供temperature属性和onTemperatureChange 属性。他将会更改本地状态来处理这些变化,而后重新用新值渲染子组件。我们很快将看到Calculator的实现。

在深入Calculator的变化之前,让我们回顾对TemperatureInput组件的改变先。我们移除了本地的state状态,从读取this.state.temperature变成读取this.props.temperature。在我们想做些变化时,从调用this.setState()变成调用父组件提供的this.props.onTemperatureChanged():

  1. class TemperatureInput extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.handleChange = this.handleChange.bind(this);
  5. }
  6. handleChange(e) {
  7. this.props.onTemperatureChange(e.target.value);
  8. }
  9. render() {
  10. const temperature = this.props.temperature;
  11. const scale = this.props.scale;
  12. return (
  13. <fieldset>
  14. <legend>Enter temperature in {scaleNames[scale]}:</legend>
  15. <input value={temperature}
  16. onChange={this.handleChange} />
  17. </fieldset>
  18. );
  19. }
  20. }

现在让我们转向Calculator组件。

我们把input的temperature和scale存在Calculator中。这是从input上提升起来的属性,它们将作为“唯一的真理之源”而存在。这是我们为了渲染两个input而所需要的最基本表达。

例如,若是在摄氏输入中输入37,Calculator组件的state将会是:

  1. {
  2. temperature: '37',
  3. scale: 'c'
  4. }

接着在华氏输入中输入212,Calculator组件的state将会是:

  1. {
  2. temperature: '212',
  3. scale: 'f'
  4. }

我们本可以将两个input的值都存起来,但结果是没有必要。存一个最近变化的的input的value值和其所代表scale就够了。我们可以根据当前的temperature和scale来推算出另一个值。

两个input现在保持同步是因为他们的值都是通过相同的state计算出的。

  1. class Calculator extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
  5. this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
  6. this.state = {temperature: '', scale: 'c'};
  7. }
  8. handleCelsiusChange(temperature) {
  9. this.setState({scale: 'c', temperature});
  10. }
  11. handleFahrenheitChange(temperature) {
  12. this.setState({scale: 'f', temperature});
  13. }
  14. render() {
  15. const scale = this.state.scale;
  16. const temperature = this.state.temperature;
  17. const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
  18. const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
  19. return (
  20. <div>
  21. <TemperatureInput
  22. scale="c"
  23. temperature={celsius}
  24. onTemperatureChange={this.handleCelsiusChange} />
  25. <TemperatureInput
  26. scale="f"
  27. temperature={fahrenheit}
  28. onTemperatureChange={this.handleFahrenheitChange} />
  29. <BoilingVerdict
  30. celsius={parseFloat(celsius)} />
  31. </div>
  32. );
  33. }
  34. }

现在,无论编辑哪个Input,Calculator中的this.state.temperature和this.state.scale都会得到更新。任何一个input得到value,用户的所有输入都会被存起,另外的input值总会基于这个值重新计算。

让我们看看当你输入时发生了什么:

  • 就像原生DOM的input标签一样,React调用指定的方法。在我们的例子中,这就是TemperatureInput组件中的handleChange。
  • TemperatureInput组件中的handleChanges用新的参数值调用this.props.onTemperatureChange()。它的props属性,包括onTemperatureChange,都是父组件Calculator提供的。
  • 当它最近一次渲染时,Calculator能明确Celsius摄氏输入的TemperatureInput属性是Calcuator的handleCelsiusChange()方法,Fahrenheit 华氏输入的TemperatureInput属性是Calcuator的handleFahrenheitChange()。这样,根据我们的输入Calculator调用其中的一个方法。
  • 这些方法中,Calculator组件通过使用新的input中的value值调用setState(),且当前的input的scale更新了,这样使React重新渲染。
  • React调用Calculator组件的render()方法来了解新的UI的模样。基于当前temperature和scale的两个input输入框中的值都是被重新计算的。温度装换的方法也是在这里使用的。
  • React再调用两个独立的TemperatureInput组件的render()方法,他们的props属性已经右父组件Calculator明晰。React知道UI该是什么样儿。
  • React DOM(虚拟DOM)更新原生DOM来匹配理想的值。一个是我们刚刚编辑过得input得到了它当前值,另一个是转换过温度的值得input也被更新。

每次更新的步骤都是这样,所以两个input能保持同步。

课程收获

任何在React中需要变化的数据都应该是来自于单一的“真理之源”。通常,state是第一个添加到组件中的渲染的。而后,要是另一个组件也需要这个状态,那就把改状态提升到组件们共同的祖先控件中。你应该使用这种[自顶向下数据模型](https://reactjs.org/docs/lifting-state-up.html)而不是尝试让组件间通信。

提升状态可能比两方绑定这种方式涉及写更多的“样板”代码。但是也有好处,那就是找出bug,隔离bug会轻松很多。由于任何“居住”在组件的状态是组件单独控制,那么界面上的bug就极大减少了。另外,你也能实现任何自定义逻辑注入或者转换在用户输入中。

如果有些东西需要来自于props或者state,那么它可能不该是state。举个例子,为了代替存储celsiusValue和fahrenheitValue,我们只存储了最新编辑的temperature和它的scale。另一个组件的值在render()方法中是可以通过这两个计算出来的。这样也是我们得以清除或者取整另一个而不丢失精度。(??)

当你发现一些UI上的错误,你可以使用 React Developer Tools 来检测props并在DOM树上挪一挪来找出那些正在更新state的组件。这样,你就能从源码中跟踪bug:

10. QuickStart : 状态提升 - 图1

官网文章 Quick Start :lifting state up