[译]React核心概念9:状态提高

原文连接:reactjs.org/docs/liftin…html

引言

不少状况下咱们使用的多个组件须要对同一个数据作出对应的反应。在这里咱们推荐把这个共享的状态提高到距离这些组件最近的祖先组件。如今让咱们来看看这是怎么工做的。react

在本章中,咱们将会建立一个温度计算器来计算在给定温度下水是否会沸腾。git

首先咱们现建立一个BoilingVerdict组件。它接收一个celsius(摄氏度)做为prop,并在页面上打印出在这个温度下水是否会沸腾。github

function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
}
复制代码

接下来,咱们将会建立一个Calculator组件。它渲染了一个输入框来输入温度并将输入值绑定到this.state.temperaturebash

除此以外,它也为当前输入的温度渲染相应的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>
    );
  }
}
复制代码

添加第二个输入框

如今咱们有一个新需求,除了输入摄氏温度以外,咱们还须要一个可以输入华氏温度的输入框,而且这两个输入框是同步的。ui

首先咱们能够从Calculator组件中提取出一个TemperatureInput组件。并添加一个scale做为prop来表明摄氏度(c)或华氏温度(f)。this

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组件来渲染两个不一样的温度输入。spa

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字符串和转换函数做为参数并返回一个字符串。咱们使用这个函数根据其中一个输入框的数据来计算另外一个输入框的数据。

同时若是输入的温度无效那么就返回一个空字符串,对有效的温度返回精确到小数点后三位的数字。

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转移到离须要共享组件最近的公共父组件中,这称为“状态提高”。如今咱们将把TemperatureInput组件中的state转移到Calculator中。

若是Calculator包含了共享状态,那么它就是这两个温度输入组件的“数据源”,这能使两个输入组件的数据始终保持一致。 由于两个TemperatureInput组件的props都是来自共同的父组件Calculator的,因此他们在数据显示上可以保持同步。

如今咱们来逐步了解这是怎么完成的。

首先,咱们将TemperatureInput组件中的this.state.temperture替换成this.props.temperature。固然this.props.temperature是经过Calculator组件传递的。

render() {
    // Before: const temperature = this.state.temperature;
    const temperature = this.props.temperature;
    // ...
复制代码

咱们知道props是只读的,当咱们将温度存储在本地state时,咱们能够经过setState()来修改它。可是如今温度是经过父组件传递进来的,那么TemperatureInput组件就没有修改温度的权限了。

React中,一般的解决方案是将组件变为“受控”的。就像在DOM中<input>接收valueonChange做为prop同样,咱们可让TemperatureInput组件接收从Calculator传递来的prop:temperatureonTemperatureChange

如今,当TemperatureInput想要更新温度时,只要调用this.props.onTemperatureChange就好了:

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

提示 temperature和onTemperatureChange在这里没有特殊的含义,只是一个属性名称,是能够随意定义的,它也能够是更大众化的value和onChange。

onTermperatureChange属性和temperature属性都是由父组件Calculator提供的。它会经过修改自身的state来处理数据的变化,以此来从新渲染两个输入框内的数据。咱们很快就能够看到Calculator组件的实现细节。

在咱们深刻了解Calculaor组件的实现以前,先让咱们来回顾一下TemperatureInput组件作了哪些修改。咱们将移除了本地state,将本来经过读取this.state.temperature获取温度替换成读取this.props.temperature获取,将经过调用setState()修改数据替换成调用this.props.onTemperatureChange()修改数据,这两种都由Calculator组件提供。

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中,这是咱们从input中提高出来的状态,来做为两个input的数据源。这就是咱们渲染两个input须要的最小数据集合。

举个例子,如今咱们在摄氏度输入框内输入37,那么Calculator组件的state就将会是这样:

{
  temperature: '37',
  scale: 'c'
}
复制代码

若是以后咱们在华氏温度输入框中输入212,Calculator的state就将变成:

{
  temperature: '212',
  scale: 'f'
}
复制代码

有人会说为何存储两个input的值?能够,但不必。只要存储最近修改的值和它所表明的scale就足够了。由于咱们能够根据当前的temperature和scale推算出另外一个input的值。

如今,输入框的值是同步的了,由于它们的值都是由同一状态计算出来的。

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.scalethis.state.temperature都会更新。其中一个输入框的输入按照原样取值,因而用户的输入就这样被保存了,而另外一个输入框的值就会根据用户的输入推算出来。

让咱们梳理如下当咱们在一个输入框输入时代码的运行逻辑:

  • React调用DOM标签<input>上的onChange方法,在咱们的例子中,调用的时TemperatureInput组件的handleChange方法。
  • TemperatureInput组件中的handleChange被调用时,它将最新的数据传递给this.props.onTemperatureChange()。这里的props都由父组件Calculator提供。
  • 以前渲染的时候,Calculator组件在摄氏度的TemperatureInput组件上声明的onTemperatureChange事件所调用的方法是handleCelsiusChange,在华氏温度上的则是handleFahrenheitChange。因此这两个函数的调用取决于咱们在哪一个输入框输入数据。
  • 在这些方法中,Calculator组件调用setState()将最新输入的值和scale从新渲染。
  • React调用Calculator组件的render()方法将UI渲染在页面上。两个输入框的值都在此时根据当前的状态从新计算,温度的换算也在此时进行。
  • React调用两个TemperatureInput组件的render()方法,并根据由Calculator定义的props渲染在页面上。
  • React调用BoilingVerdict组件的render()方法,并根据传进来的摄氏度温度渲染数据。
  • React DOM更新沸腾结果和输入框的内容。咱们刚刚编辑的输入框接收当前的值,而另外一个输入框接收通过转换的值。

每一次更新都按照上述步骤进行,因此两个输入框数据能够保持同步。

经验总结

在React应用中任何可变的数据都应该有惟一数据源。一般来讲,state一开始都是包含在须要根据它来渲染数据的组件中。可是若是有其余组件也想要使用这个state,那么就须要把它提高到距离这两个组件最近的公共父组件中。可是相比于保持不一样组件的数据同步,咱们更应该依靠的是自顶而下的数据流。

相较于双向绑定,状态提高须要编写更多的“样板”代码。但这样作的好处是咱们能够更好地定位和分离bug。由于state只存在于各自的组件中,因此bug出现范围就大大减小了。除此以外,咱们能够自由地实现任何逻辑来拒绝或修改用户输入。

若是某个数据能够根据state或者props推导出来,那么这个数据就不该该存储在state中。就像本章的实例代码中,咱们只存储最新输入的温度和它对应的scale,并不须要将华氏温度和摄氏温度都存储在state中。另外一个输入框的数据在调用render()方法时就能够根据当前的数据推算出来。这能让咱们清除用户输入或者在不损失用户输入精度的状况下在其余区域使用用户输入的值。

当页面出现错误时,你可使用React Developer Tools查看props而且在组件树上追溯源头直到找到对应的组件为止。这可以让你轻松地定位bug。

相关文章
相关标签/搜索