您好, 欢迎来到 !    登录 | 注册 | | 设为首页 | 收藏本站

React 状态提升

React 状态提升

通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。让我们看看它是如何运作的。

在本节中,我们将创建用于计算水在给定温度下是否会沸腾的温度计算器。

我们将从名为 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 组件,然后为其新的 scaleprop,它可以是 "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>
    );
  }
}

我们现在有了两个输入框,但当你在其中输入温度时,另并不会更新。这与我们的要求相矛盾:我们希望让它们保持同步。

另外,我们也不能通过 Calculator 组件展示 BoilingVerdict 组件的渲染结果。因为 Calculator 组件并不知道隐藏在 TemperatureInput 组件中的当前温度是多少。

编写转换

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

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

例如,tryConvert('abc', toCelsius) 返回空字符串,而 tryConvert('10.22', toFahrenheit) 返回 '50.396'。

状态提升

到目前为止, 两个 TemperatureInput 组件均在各自内部的 state 中相互独立地保存着各自的数据。

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;
    // ...

然而,我们希望两个输入框内的数值彼此能够同步。当我们更新摄氏度输入框内的数值时,华氏度输入框内应当转换后的华氏温度,反之亦然。

在 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;
    // ...

我们知道 props 是只读的。当 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);
    // ...

注意:

组件中的 temperature 和 onTemperatureChange 这两个 prop 的命名没有任何特殊含义。我们可以给它们取其它任意的名字,例如,把它们命名为 value 和 onChange 就是一种习惯。

onTemperatureChange 的 prop 和 temperature 的 prop 一样,均由父组件 Calculator 提供。它通过父组件自身的内部 state 来处理数据的变化,进而使用新的数值重新渲染两个输入框。我们将很快看到后的 Calculator 组件。

在深入研究 Calculator 组件的变化之前,让我们回顾一下 TemperatureInput 组件的变化。我们移除组件自身的 state,通过使用 this.props.temperature 替代 this.state.temperature 来读取温度数据。当我们想要响应数据改变时,我们需要 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>
    );
  }
}

现在,让我们把目光转向 Calculator 组件。

我们会把当前输入的 temperature 和 scale 保存在组件内部的 state 中。这个 state 就是从两个输入框组件中“提升”而来的,并且它将用作两个输入框组件的共同“数据源”。这是我们为了渲染两个输入框所需要的所有数据的最小表示。

例如,当我们在摄氏度输入框中键入 37 时,Calculator 组件中的 state 将会是:

{
  temperature: '37',
  scale: 'c'
}

如果我们之后华氏度的输入框中的为 212 时,Calculator 组件中的 state 将会是:

{
  temperature: '212',
  scale: 'f'
}

我们可以存储两个输入框中的值,但这并不是必要的。我们只需要存储最近的温度及其计量单位即可,根据当前的 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>
    );
  }
}

现在无论你编辑哪个输入框中的,Calculator 组件中的 this.state.temperature 和 this.state.scale 均会被更新。其中输入框保留的输入并取值,另输入框始终基于这个值转换后的结果。

让我们来重新梳理一下当你对输入框进行编辑时会发生些什么:

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 的排查范围被大大缩减了。此外,你也可以使用逻辑来拒绝或转换的输入。

如果某些数据可以由 props 或 state 推导得出,那么它就不应该存在于 state 中。举个例子,本例中我们没有将 celsiusValue 和 fahrenheitValue 一起保存,而是仅保存了最后的 temperature 和它的 scale。这是因为另输入框的温度值始终可以通过这两个值以及组件的 render() 获得。这使得我们能够清除输入框,亦或是,在不损失操作的输入框内数值精度的前提下对另输入框内的转换数值做四舍五入的操作。

当你在 UI 中发现时,可以使用  来检查问题组件的 props,并且按照组件树结构逐级向上搜寻,直到定位到负责更新 state 的那个组件。这使得你能够追踪到产生 bug 的源头:



联系我
置顶