sfrog

状态提升 - React

sfrog · 2016-12-10翻译 · 839阅读 原文链接

我们经常遇到这种情况,好几个组件需要用到相同的变量。我们建议在这种情况下把这些变量提升到离这几个组件最近的公共父组件中。下面让我们来看看具体如何做。

首先,我们创建一个温度计算器,它可以根据给定的温度来计算水是否会沸腾。

我们从BoilingVerdict组件开始,它接收一个叫做celsius的温度参数,然后根据这个温度返回水是否会沸腾。

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

在CodePen上试试

再添加一个input #

我们的新需求是:除了提供一个摄氏温度的input,我们还需要提供一个华氏温度的input,然后这两个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组件重构了,由两个不同的TemperatureInput组件组成:

class Calculator extends React.Component {
  render() {
    return (
      <div>
        <TemperatureInput scale="c" />
        <TemperatureInput scale="f" />
      </div>
    );
  }
}

在CodePen上试试

现在我们有了两个input,但是当你往其中任何一个input中输入温度值时,另外的一个input并不会更新,这显然与我们的需求不符,需求是要让它们同步更新。

同时,我们也没法在Calculator中渲染BoilingVerdict,因为Calculator并不知道当前输入的温度值是多少,这个值被隐藏在TemperatureInput中。

状态提升 #

首先,我们需要两个函数来完成摄氏度和华氏度之间的互相转换:

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

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

上面的这两个函数只能做数字转换,所以我们还需要另外一个函数来将一个input的字符串值转换成另一个input的字符串值。这个函数接收一个输入字符串和一个转换函数作为参数,返回转换之后的字符串。

如果输入的字符串参数不能被转换成数字,则返回空字符串。返回的字符串数字四舍五入到三位小数:

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去掉,把valueonChange都替换成用props来传递:

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,就是需要做状态提升的信号,要把这些state提升到离这些组件最近的公共父组件中。在我们的示例中,这个公共父组件就是Calculator。所以,我们现在把valuescale放到Calculator中。

我们也可以把两个input中的值都存下来,但是这并没有必要。知道最近使用的input值和温度单位就可以计算出另外一个input值。

这样两个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上试试

现在不管你在哪个input中输入,Calculator中的this.state.valuethis.state.scale都会被更新。任何用户的输入都展示在被输入的那个input中,另外一个input则展示根据用户的输入计算过来的值。

学到了什么 #

对于在React应用中那些变化的数据,有这么一个真理:通常来讲,state一开始是放在需要渲染它的那个组件中,一旦另外的组件也需要这个state,那么,这个state就需要被提升到离这些组件最近的公共父组件中,而不是把这个state在这些组件之间同步,也就是说,你需要遵循自顶向下的数据流

虽然跟双向绑定的方式相比,状态提升的方式需要写更多的代码,但是你能更容易地找到和解决Bug。因为状态提升的方式让你只在拥有这个State的组件中改变这个State,所以一旦出现Bug,需要关注的组件就是少数几个管理state的组件。另外,用这种方式你还可以在更新state之前使用自定义逻辑来决定是否需要拒绝或者转换用户的输入。

如果某个值可以根据其它的props或state计算得到,那么一般来讲就不应该成为一个state。用上面的例子来讲,我们只存储了最后被编辑的value和它的scale,而不是分别存储了摄氏温度和华氏温度,因为我们可以在render()方法中根据其中一个的值和单位来计算出另外一个的值。而且这样我们还可以在使用的时候才决定是否使用舍入和如何使用舍入来展示这个值。

当你在UI上发现一些错误的时候,你可以使用React Developer Tools来检查参数,顺着树形结构一直往上查找,直到找到负责更新这个状态的组件。这样你就可以跟踪到问题的根源:

Monitoring State in React DevTools

译者sfrog尚未开通打赏功能

相关文章