【React入门学习】11.状态提升

242 阅读4分钟

状态提升

参考文档:react.docschina.org/docs/liftin…

通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。让我们看看它是如何运作的。创建一个用于计算水在给定温度下是否会沸腾的温度计算器。

将从一个名为 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>
                );
            }
        }
        ReactDOM.render(
            <Calculator />,
            document.getElementById('root')
        )

编写转换函数

首先,我们将编写两个可以在摄氏度与华氏度之间相互转换的函数:

function toCelsius(fahrenheit) {
  return (fahrenheit - 32) * 5 / 9;
}

function toFahrenheit(celsius) {
  return (celsius * 9 / 5) + 32;
}

上述两个函数仅做数值转换。编写另一个函数,它接受字符串类型的 temperature 和转换函数作为参数并返回一个字符串。我们将使用它来依据一个输入框的值计算出另一个输入框的值。

当输入 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();
}

状态提升

在 React 中,将多个组件中需要共享的 state 向上移动到它们的最近共同父组件中,便可实现共享 state。这就是所谓的“状态提升”。接下来,我们将 TemperatureInput 组件中的 state 移动至 Calculator 组件中去。

如果 Calculator 组件拥有了共享的 state,它将成为两个温度输入框中当前温度的“数据源”。它能够使得两个温度输入框的数值彼此保持一致。由于两个 TemperatureInput 组件的 props 均来自共同的父组件 Calculator,因此两个输入框中的内容将始终保持一致。

首先,我们将 TemperatureInput 组件中的 this.state.temperature 替换为 this.props.temperature。现在,我们先假定 this.props.temperature 已经存在,尽管将来我们需要通过 Calculator 组件将其传入:

  render() {
    // Before: const temperature = this.state.temperature;
    const temperature = this.props.temperature;    // ...

当 temperature 存在于 TemperatureInput 组件的 state 中时,组件调用 this.setState() 便可修改它。然而,temperature 是由父组件传入的 prop,TemperatureInput 组件便失去了对它的控制权。

在 React 中,这个问题通常是通过使用“受控组件”来解决的。与 DOM 中的 <input> 接受 value 和 onChange 一样,自定义的 TemperatureInput 组件接受 temperature 和 onTemperatureChange 这两个来自父组件 Calculator 的 props。

现在,当 TemperatureInput 组件想更新温度时,需调用 this.props.onTemperatureChange 来更新它:

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

我们想要响应数据改变时,我们需要调用 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>
    );
  }
}

我们可以存储两个输入框中的值,但这并不是必要的。我们只需要存储最近修改的温度及其计量单位即可,根据当前的 temperature 和 scale 就可以计算出另一个输入框的值。

由于两个输入框中的数值由同一个 state 计算而来,因此它们始终保持同步:

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>
    );
  }
}

image.png