建议配合官网文档Lifting State Up进行使用。

Lifting State Up

通常,一些组件会反应相同的数据,我们推荐将它们反应的共享状态提升至它们最近的公共父组件。

我们将创建一个温度计算组件,来判断温度是否能使水沸腾。

首先,我们创建名为BoilingVerdict的组件,它接收celsius作为参数,并且输出该温度是否能使水沸腾:

function BoilingVerdict(props) {
	if (props.celsius > 100) {
		return <p>The water would boil.</p>;
	}
	else {
		return <p>The water would not boil.</p>;
	}
}

接着,我们创建名为Calculator的组件,它提供一个<input>让我们输入温度,并将温度保存在state中:

class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.state = {temperature: ''};
  }

  handleChange = ()=> {
    this.setState({temperature: event.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>
    );
  }
}

Adding a Second Input

现在添加一个新需求:我们额外提供一个华氏度的输入框,摄氏度和华氏度保持同步变化。

因此,我们可以将上述的摄氏度输入框提取为温度输入组件TemperatureInput,并给其传递一个名为scale的 prop,该参数可以是cf,用以表示输入为摄氏度还是华氏度:

const scaleNames = {
  c: "Celsius",
  f: "Fahrenheit"
}
class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      temperature: 0,
    }
  }
  handleChange = () => {
    this.setState({temperature: event.target.value})
  }

  render() {
    return (
      <filedset>
        <legend>Enter temprature in {scaleNames[this.props.scale]}:</legend>
        <input value={this.state.temperature} onChange={this.handleChange} />
        </filedset>
    )
  }
}

现在我们修改Calculator组件,使之包含华氏度输入组件和摄氏度输入组件:

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

现在我们拥有了两个温度输入框,但是当我们向其中一个输入框输入温度时,另一个温度并不会变化。

Writing Conversion Functions

首先,我们需要编写一个转化函数,用来进行摄氏度、华氏度的相互转化:

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

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

这两个函数是数字之间的转换,我们还会写一个另外的函数,获取字符串温度temperature,和一个转换函数作为参数,实现字符串温度之间的转换,我们将使用这个另外的函数来实现两个温度之间的单位换算。

对于不合理的温度,我们返回空字符串,我们将保留三位小数。

function tryConvert(temperature, converter) {
  const input = parseFloat(temperature);
  if (Number.isNaN(input)) {
    return '';
  }
  const output = converter(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}

例如tryConvert('10.22', toFahrenheit),函数将返回"50.396"

Lifting State Up

现在,两个TemperatureInput独立的维护自己的温度状态,然而我们希望当我们改变摄氏度时,华氏度能同步更新,反之亦然。

在 React 中,共享状态往往通过将其提升至需要共享的组件的最近的祖先组件中实现的,我们称之为状态提升lifting state up。因此,我们将TemperatureInput中的temperature提升至父组件Calculator中。

Calculator维护着共享状态时,对于其下的两个温度输入框,它们的从父组件获取的温度时同源的,因此这两个输入框的数据一定时同步的。

让我们按步骤来研究它的实现过程:

  1. 首先将Temperature中的温度应该是从参数中获得,因此将this.state.temperature改为this.props.temperature
  2. 我们知道参数是只读的,换句话说,在子组件中无法修改父组件的温度状态,而只能父组件本身调用setState函数来修改,因此子组件除了接收温度作为参数,还需要接收一个能修改温度的函数onTemperatureChange,当子组件希望改变父组件的参数时,可以调用该函数来实现父组件对状态的修改。
  3. 具体来说,onTemperatureChange接收一个温度作为参数,父组件调用该函数修改自己的状态,并且重新渲染这两个子组件。

代码实现如下:

class Temperature extends React.Component {
  constructor(props) {
    super(props);
  }

  return (
    <fieldset>
      <legend>Enter temperature in {scaleNames[this.props.scale]}:</legend>
        <input value={temperature}
               onChange={this.props.onTemperatureChange} />
    </fieldset>
  );
}

对于Calculator来说,虽然他需要向摄氏度和华氏度传递两个不同的温度数值,但由于摄氏度和华氏度可以相互转换,因此只需要维护一份温度数据即可,可以仅维护摄氏度或仅维护华氏度,也可以维护最近修改的温度,官网文档采用了最后一种方式,例如:

  • 我们在摄氏度输入框中输入 37,则Calculator的状态为:

    {
    temperature: 37,
    scale: "c"
    

}:hexoPostRenderEscape–>

  • 我们在华氏度输入框中输入 212,则Calculator的状态为:

    {
    temperature: 212,
    scale: "f"
    

}:hexoPostRenderEscape–>

若仅存储摄氏度或华氏度时,当用户的输入不为对应的格式时,我们需要经过一步转化才能存储,这样我们实际上存储的数据并不是用户本身的输入,因此可能会造成一些数据丢失。在本例中,温度的转换可能会损失精度。

对于Calculator如何辨别修改的是摄氏度输入框,还是华氏度输入框,既可以通过子组件向onTemperatureChange()额外传递参数,也可以编写两个函数分别控制摄氏度变化,华氏度变化,官方文档使用了后一种方法:

class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      scale: "c",
      temperature: "",
    }
  }
  // 编写传递给摄氏度输入框的函数
  handleCelsiusChange = (temperature) => {
    this.setState({
      scale: 'c',
      temperature: temperature
    })
  }
  handleFahrenheitChange = (temperature) => {
    this.setState({
      scale: 'f',
      temperature: temperature
    })
  }

  render() {
    const celsiusTemperature = this.scale === 'c'
      ? this.temperature
      : tryConvert(this.temperature, toCelsius);
    const fahrenheitTemperature = this.scale === 'f'
      ? this.temperature
      : tryConvert(this.temperature, toFahrenheit);
    return (
      <div>
        <TemperatureInput
          scale="c"
          temperature={celsiusTemperature}
          onTemperatureChange={this.handleCelsiusChange}
        <TemperatureInput
          scale="f"
          temperature={fahrenheitTemperature}
          onTemperatureChange={this.handleFahrenheitChange}
      </div>
    )
  }
}

现在,无论你编辑哪一个输入框,另一个输入框也会随之改变。