提升状态

通常,几个组件需要根据同一个数据变化做出响应。我们建议将这个共享的状态提升到他们最近的一个共用祖先。让我们看看实际该怎么做。
在这一节,我们将创建一个温度计算器,用来计算一给定温度能否让水沸腾。
我们从名为BoilingVerdict的组件开始。它接受celsius温度作为prop,然后打印出是否足够使水沸腾:

function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return 

The water would boil.

; } return

The water would not boil.

; }

接下来,我们创建一个Calculator组件。它渲染一个供你输入温度的,并将它的值存在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 (
      
Enter temperature in Celsius:
); } }

在CodePen上试一试

添加第二个输入

我们有个新的需求,除了一个摄氏度输入,我们还要提供一个华氏输入,并且他们保持同步。
我们先从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 (
      
Enter temperature in {scaleNames[scale]}:
); } }

现在我们可以改变Calculator来渲染两个独立的温度输入:

class Calculator extends React.Component {
  render() {
    return (
      
); } }

在CodePen上试一试
现在我们有两个输入了,但是当你在其中一个里输入温度,另一个不会去更新。这就不满足我们的需求了,我们想让他们同步。
我们也没在Calculator中显示BoilingVerdictCalculator不知道当前的温度,因为温度被隐藏在TemperatureInput中。

编写转换函数

首先我们写两个函数来互相转换摄氏度和华氏温度。

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

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

这两个函数转换数字。接下来我们写另一个函数,接受一个temperature字符串和一个转换函数作为参数,返回一个字符串。我们将用他来根据另一个input来计算这个input的值。
一个无效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组件都单独地在本地状态中保存自己的值:

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;

然而,我们希望这两个input可以彼此同步。当我们更新摄氏度input,华氏度input也会相应的转换温度,反之亦然。
在React中,想要共享状态,就需要找到共享状态组件的一个最近共有祖先,然后通过将该状态移动到这个共有祖先上来完成共享。这称为“提升状态”。我们先移除TemperatureInput的本地状态,取而代之的是将它移到Calculator中。
如果Calculator拥有共享状态,对于两个input中的温度来说他就成为了“真相的来源”。他就可以指示两个input具有相同的值。由于两个TemperatureInput组件的props都来自同一个父组件Calculator,两个input将始终保持同步。
让我们逐步分析这是如何工作的。
首先,在TemperatureInput组件中,我们使用this.props.temperaturethis.state.temperature替换掉。现在,让我们假设this.props.temperature已经存在,之后我们会从Calculator中传入该值:

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

我们知道props是只读的。之前temperature在本地状态中,TemperatureInput只能调用this.setState()来改变它。而现在,temperature作为prop从父组件获取,TemperatureInput不能再控制它了。
在React中,一般通过将组件变为“受控”,来解决这个。就像DOM接受一个value和一个onChangeprop,所以自定义的TemperatureInput可以从它的父组件Calculator中获取temperatureonTemperatureChangeprops。
现在,当TemperatureInput想要更新它的温度值,调用this.props.onTemperatureChange就好了:

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

注意,自定义组件中的prop名字:temperatureonTemperatureChange并没什么特别的意思。我们可以随意命名,比如给它们更通用的名字valueonChange
父组件Calculator提供proponTemperatureChange的同时也提供temperature。他通过修改自己的本地状态来处理更改,从而将两个input重新渲染为新的值。我们马上就来看看新的Calculator实现。
在深入Calculator的变化之前,我们来重新看遍TemperatureInput组件中做过什么变动。我们将他的本地状态移除,将从this.state.temperature读取,改为从this.props.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 (
      
Enter temperature in {scaleNames[scale]}:
); } }

现在让我们回到Calculator组件。
我们将当前输入的temperaturescale存到他的本地状态。这就是我们从input中“提升”的状态,该状态将作为“真相的来源”提供给两个TemperatureInput。这是我们渲染两个input,所需数据的最少表示。
假如,我们在摄氏度输入框中输入37,Calculator组件的状态如下:

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

如果我们将华氏字段编辑为212,Calculator的状态将变为:

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

我们可以存储两个输入的值,但实际上是不必的。存储最后一次变化的值和单位即可。然后我们可以根据当前的温度和单位来换算出另一个值。
因为两个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 (
      
); } }

在CodePen上试一试
现在,不论你编辑哪一个input,Calculator中的this.state.temperaturethis.state.scale都会得到更新。从任何一个input获取的用户输入都会被保存,另一个input的值会根据它重新计算。
让我们重新看下当你编辑一个input时发生了什么:

  • React调用DOM上指定为onChange的函数。在我们的例子中这个函数是TemperatureInput组件的handleChange方法。
  • TemperatureInput中的handleChange方法,使用新的需求值调用this.props.onTemperatureChange()。他的props,包括onTemperatureChange,都是由他的父组件Calculator提供。
  • 在渲染之前,Calculator已经将自己的handleCelsiusChange方法赋值给摄氏度TemperatureInputonTemperatureChange,还将自己的handleFaherenheitChange方法赋值给华氏度TemperatureInputonTemperatureChange。所以,Calculator的这个两个方法会根据我们编辑的input,而得到调用。
  • 在这些方法中,Calculator组件通过使用新的输入值和在编辑中input的单位来调用this.setState(),使得React重新渲染自己。
  • React通过调用Calculator组件的render方法来获取UI的外观。两个input的值根据当前的温度和单位重新计算。温度的换算在这个时候进行。
  • React根据Calculator提供的新props来分别调用TemperatureInputrender方法。由此得知他们UI的外观。
  • React DOM更新DOM来匹配所需的input值。我们编辑的input接受当前的值,另一个input更新为转换后的问题。

每次更新都会重复上面的步骤,从而使所有input保持同步。

经验教训

在React应用中,所有变化的数据都应该是单独的“真相来源”。通常,状态第一个被添加到组件中(组件需要用这些状态来进行渲染)。如果其他组件也需要它,你可以将状态提升到这些组件共用的最近祖先。你应该依赖自上而下的数据流,而不是同步多个组件的状态。
比起双向绑定的方法,提升状态需要写更多的“样板”代码,但好处就是,它可以更轻松的找到和隔离bug。因为任何状态都是存在于组件中,并且只有组件可以修改它,由此bugs存在的区域大大被减少。另外,你可以实现任意逻辑来拒绝或转换用户的输入。
如果某个值可以通过其他props或状态获得,那他就不该把他放在状态中。比如,我们仅仅存储上一次编辑的温度和单位,而不是将celsiusValuefahrenheitValue都存下来。因为在render()方法中,一个input的值始终可以通过另一个计算得来。这样,我们对另一个字段用或不用四舍五入,都不会在用户的输入中丢失精度。
当你发现UI上有错误发生,你可以使用React开发者工具来检查props,然后沿着树结构向上,知道你找到负责更新状态的组件。这使你追溯到bug的来源:

提升状态_第1张图片

你可能感兴趣的:(提升状态)