Lifting State Up

提升状态

通常,几个组件需要反映相同的变化数据。我们建议将共享状态提升至最接近的共同祖先。让我们看看这是如何运作的。

在本节中,我们将创建一个温度计算器,用于计算在给定温度下水是否沸腾。

我们将从一个叫做组件开始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.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 ( <fieldset> <legend>Enter temperature in Celsius:</legend> <input value={temperature} onChange={this.handleChange} /> <BoilingVerdict celsius={parseFloat(temperature)} /> </fieldset> } }

在 CodePen 上试用它。

添加第二个输入

我们的新要求是,除了摄氏温度输入外,我们还提供华氏温度输入,并且它们保持同步。

我们可以从提取TemperatureInput组件开始Calculator。我们将添加一个新的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 = {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以呈现两个单独的温度输入:

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

在 CodePen 上试用它。

我们现在有两个输入,但是当你在其中一个输入温度时,另一个不会更新。这与我们的要求相矛盾:我们希望保持同步。

我们也无法显示BoilingVerdictCalculator。在Calculator不知道当前的温度,因为它是藏在里面的TemperatureInput

编写转换函数

首先,我们将编写两个函数将摄氏温度转换为华氏温度,然后返回:

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

这两个函数转换数字。我们将编写另一个函数,它将字符串temperature和转换器函数作为参数并返回一个字符串。我们将使用它来计算基于其他输入的一个输入的值。

它会在无效的情况下返回一个空字符串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; // ...

但是,我们希望这两个输入互相同步。当我们更新摄氏温度输入时,华氏温度输入应反映转换后的温度,反之亦然。

在 React 中,共享状态是通过将它移动到需要它的组件的最接近的共同祖先来完成的。这被称为“提升状态”。我们将从当地国家中移除TemperatureInput并将其移入Calculator

如果Calculator拥有共享状态,它将成为两个输入中当前温度的“真值源”。它可以指导他们都有相互一致的价值观。由于两个TemperatureInput组件的道具来自同一个父Calculator组件,因此两个输入始终保持同步。

让我们看看这是如何一步一步工作。

首先,我们将替换this.state.temperaturethis.props.temperatureTemperatureInput部件。现在,让我们假装this.props.temperature已经存在,尽管我们需要Calculator在未来将它传递出去:

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

我们知道道具是只读的。当temperature在当地的状态,TemperatureInput可以打电话this.setState()来改变它。但是,现在temperature来自父母的道具,TemperatureInput它无法控制它。

在 React 中,通常通过将组件“控制”来解决这个问题。就像 DOM <input>接受a value和onChangeprop一样,自定义也可以TemperatureInput接受它的父项temperature和onTemperatureChange道具Calculator。

现在,当TemperatureInput想要更新其温度时,它会调用this.props.onTemperatureChange

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

注意:对自定义组件中的任一temperatureonTemperatureChange名称没有特殊含义。我们可以称其他任何东西,比如说它们的名字valueonChange这是一个通用的惯例。

onTemperatureChange支柱将与一起提供temperature由父支柱Calculator组件。它将通过修改其自身的本地状态来处理更改,从而使用新值重新呈现两个输入。我们Calculator很快就会看到新的实施。

在深入了解变化之前Calculator,让我们回顾一下对TemperatureInput组件的更改。我们已经从中删除了当地的国家,而不是阅读this.state.temperature,我们现在阅读this.props.temperaturethis.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组件。

我们将存储当前的输入temperaturescale处于本地状态。这是我们从投入中“提起来”的状态,它将成为两者的“真相之源”。它是我们为了呈现两个输入而需要知道的所有数据的最小表示。

例如,如果我们在摄氏度输入中输入37,则Calculator组件的状态将为:

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

如果我们稍后编辑华氏场为212,那么Calculator将会是:

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

我们可以存储两个输入的值,但事实证明这是不必要的。存储最近更改的输入的值以及它所表示的比例就足够了。然后,我们可以基于当前temperaturescale单独推断另一个输入的值。

输入保持同步,因为它们的值是从相同的状态计算得出的:

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

在 CodePen 上试用它。

现在,无论您编辑哪个输入,this.state.temperaturethis.state.scaleCalculator获取更新。其中一个输入按原样得到值,因此任何用户输入都会保留,而另一个输入值总是基于此重新计算。

让我们回顾一下编辑输入时会发生的情况:

  • React 调用onChange在 DOM 上指定的函数<input>。在我们的例子中,这是组件中的handleChange方法TemperatureInput。

  • 组件中的handleChange方法使用新的期望值TemperatureInput调用this.props.onTemperatureChange()。它的道具,其中包括onTemperatureChange,由其母公司提供的Calculator

  • 当它先前渲染中,Calculator已经指定onTemperatureChange了摄氏的TemperatureInputCalculatorhandleCelsiusChange方法,以及onTemperatureChange所述华氏TemperatureInputCalculatorhandleFahrenheitChange方法。所以Calculator根据我们编辑的输入来调用这两个方法。

  • 在这些方法中,Calculator组件要求 React 通过调用this.setState()新的输入值和我们刚刚编辑的输入的当前比例重新呈现自己。

  • React 调用Calculator组件的render方法来了解 UI 的外观。根据当前温度和活动比例重新计算两个输入的值。温度转换在这里执行。

  • React 用它们指定的新道具调用render各个TemperatureInput组件的方法Calculator。它了解他们的用户界面应该是什么样子。

  • React DOM 更新 DOM 以匹配所需的输入值。我们刚刚编辑的输入接收其当前值,另一个输入更新为转换后的温度。

每次更新都经历相同的步骤,以使输入保持同步。

得到教训

对于在 React 应用程序中更改的任何数据,应该有一个“真相源”。通常,首先将状态添加到需要渲染的组件中。然后,如果其他组件也需要它,可以将它提升到最接近的共同祖先。与其试图在不同组件之间同步状态,您应该依赖自顶向下的数据流。

提升状态涉及编写比双向绑定方法更多的“样板”代码,但作为一个好处,查找和隔离错误需要较少的工作。由于任何状态“存在于”某个组件中,并且该组件本身可以改变它,所以错误的表面积大大降低。另外,您可以实现任何自定义逻辑来拒绝或转换用户输入。

如果某件事可以从道具或状态中推导出来,那么它可能不应该处于这个状态。例如,而不是存储既celsiusValuefahrenheitValue,我们只是存储上次编辑temperaturescale。其他输入的值可以始终由render()方法中的值来计算。这让我们可以清除或应用舍入到其他字段,而不会丢失用户输入的任何精度。

当您在 UI 中发现错误时,可以使用 React Developer Tools 检查道具并向上移动树,直到找到负责更新状态的组件。这可以让你追踪错误来源: