9. Lifting State Up(状态提升)

2017-02-17  本文已影响0人  前端xiyoki

React版本:15.4.2
**翻译:xiyoki **

通常几个组件需要响应相同的数据变化。我们建议将共享状态提升到最接近的共同祖先。让我们看看这是如何工作的。
在本节中,我们将创建一个温度计算器,用于计算水是否在给定温度下沸腾。
我们将从名为BoilingVerdict的组件开始。它接受celsius温度为props,并打印是否足以将水烧开:

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

Adding a Second Input(增加第二个输入)

我们新的要求是:除了摄氏度输入框,我们还提供华氏度输入框,并且二者是同步的。
我们把从Calculator中提取一个TemperatureInput组件作为开始。我们将为它增加一个新scale prop,并且这个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 = {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来渲染两个独立的温度输入:

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

现在我们有两个输入框,但当你向其中一个输入框输入温度时,另一个并不会更新。这违反了我们的要求:我们希望它们保持同步。
我们也不能从Calculator中展示BoilingVerdictCalculator也不知道当前的温度,因为当前温度被隐藏在了TemperatureInput内部。

Lifting State Up(状态提升)

首先,我们将写两个函数来将摄氏度转换为华氏度,将华氏度转换为摄氏度:

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

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

这两个函数转换数字。我们将写另一个函数,它接受一个字符串value和一个转换器函数作为参数,并返回一个字符串。我们将使用它来计算其中一个输入框的值,而该输入框的值基于另一个输入框。
它返回一个无效的空字符串value,并且它将输出四舍五入到三位小数:

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中删除状态。
相反,TemperatureInput组件将接受valueonChange处理程序作为prop:

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

如果几个组件需要访问相同的状态,这标志着状态应该被提升到最接近的共同祖先。在这个例子中最接近的祖先就是Calculator。我们将在它的状态中存储当前的valuescale
我们可以存储两个输入的值,但事实证明这是不必要的。它足以存储最近被更改的输入框的值,以及其表示的scale。然后我们能基于当前的valuescale,单独推断其他输入框的值。
输入的值保存同步,因为它们的值从相同的状态计算而来。

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; {/* 对状态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>
    );
  }
}

不论你编辑哪个输入框,Calculator中的this.state.valuethis.state.scale都会获得更新。其中一个输入框获取的值为原样,因此任何用户的输入都被保留,另一个输入框中的值总是基于它重新计算。

Lessons Learned (得到的教训)

对于在React应用程序中更改的任何数据,应该有一个单一的‘真实来源’。通常,首先将状态添加到需要渲染的组件。然后,如果其他组件也需要它,你可以将其提升到最接近的共同祖先。而不是尝试在不同组件之间同步状态,你应该依赖于自上而下的数据流。
提升状态涉及编写比双向绑定方法更多的‘样板’代码。但好处是找到和隔离bug需要较少的工作。由于任何状态存在于特定的组件中,并且该组件可以单独改变它,所以大大减少了错误的表面积。此外,你可以实现任何自定义逻辑以拒绝或转换用户输入。
如果数据可以从props或state派生,那么它就不应该在状态之中。例如,我们只存储了最后编辑的valuescale,而不是存储两个celsiusValuefahrenheitValue。另一个输入的值总是可以从render()方法中计算出来。这允许我们清除或应用四舍五入到其他字段,而不会丢失用户输入的任何精度。
当你在UI中看到错误时,可以使用 React Developer Tools 检查props,并向上移动树,直到找到负责更新状态的组件。这使你可以跟踪错误到其来源:

上一篇下一篇

猜你喜欢

热点阅读