通常,几个组件需要反映相同的变化数据。我们建议将共享状态提升至最接近的共同祖先。让我们看看这是如何运作的。
在本节中,我们将创建一个温度计算器,用于计算在给定温度下水是否沸腾。
我们将从一个叫做组件开始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> ); } }
添加第二个输入
我们的新要求是,除了摄氏温度输入外,我们还提供华氏温度输入,并且它们保持同步。
我们可以从提取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> ); } }
我们现在有两个输入,但是当你在其中一个输入温度时,另一个不会更新。这与我们的要求相矛盾:我们希望保持同步。
我们也无法显示BoilingVerdict
from Calculator
。在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.temperature
用this.props.temperature
的TemperatureInput
部件。现在,让我们假装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
和onChange
prop一样,自定义也可以TemperatureInput
接受它的父项temperature
和onTemperatureChange
道具Calculator
。
现在,当TemperatureInput
想要更新其温度时,它会调用this.props.onTemperatureChange
:
handleChange(e) {
// Before: this.setState({temperature: e.target.value}); this.props.onTemperatureChange(e.target.value); // ...
注意:
对自定义组件中的任一
temperature
或onTemperatureChange
名称没有特殊含义。我们可以称其他任何东西,比如说它们的名字value
,onChange
这是一个通用的惯例。
该onTemperatureChange
支柱将与一起提供temperature
由父支柱Calculator
组件。它将通过修改其自身的本地状态来处理更改,从而使用新值重新呈现两个输入。我们Calculator
很快就会看到新的实施。
在深入研究变化之前Calculator
,让我们回顾一下对TemperatureInput
组件的更改。我们已经从中删除了当地的国家,而不是阅读this.state.temperature
,我们现在阅读this.props.temperature
。this.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
处于本地状态。这是我们从投入中“提起来”的状态,它将作为两者的“真相之源”。它是我们为了呈现两个输入而需要知道的所有数据的最小表示。
例如,如果我们在摄氏度输入中输入37,则Calculator
组件的状态将为:
{
temperature: '37',
scale: 'c' }
如果我们稍后编辑华氏场为212,那么Calculator
将会是:
{
temperature: '212',
scale: 'f' }
我们可以存储两个输入的值,但事实证明这是不必要的。存储最近更改的输入的值以及它所表示的比例就足够了。然后,我们可以基于当前temperature
和scale
单独推断另一个输入的值。
输入保持同步,因为它们的值是从相同的状态计算得出的:
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> ); } }
现在,无论您编辑哪个输入,this.state.temperature
并this.state.scale
在Calculator
获取更新。其中一个输入按原样得到值,因此任何用户输入都会保留,而另一个输入值总是基于此重新计算。
让我们回顾一下编辑输入时会发生的情况:
- React调用
onChange
在DOM上指定的函数<input>
。在我们的例子中,这是组件中的handleChange
方法TemperatureInput
。 - 组件中的
handleChange
方法使用新的期望值TemperatureInput
调用this.props.onTemperatureChange()
。它的道具,其中包括onTemperatureChange
,由其母公司提供的Calculator
。 - 当它先前渲染中,
Calculator
已经指定onTemperatureChange
了摄氏的TemperatureInput
是Calculator
的handleCelsiusChange
方法,以及onTemperatureChange
所述华氏TemperatureInput
是Calculator
的handleFahrenheitChange
方法。所以Calculator
根据我们编辑的输入来调用这两个方法。 - 在这些方法中,
Calculator
组件要求React通过调用this.setState()
新的输入值和我们刚刚编辑的输入的当前比例重新呈现自己。 - React调用
Calculator
组件的render
方法来了解UI的外观。根据当前温度和活动比例重新计算两个输入的值。温度转换在这里执行。 - React 用它们指定的新道具调用
render
各个TemperatureInput
组件的方法Calculator
。它了解他们的用户界面应该是什么样子。 - React DOM更新DOM以匹配所需的输入值。我们刚刚编辑的输入接收其当前值,另一个输入更新为转换后的温度。
每次更新都经历相同的步骤,以便输入保持同步。
得到教训
对于在React应用程序中更改的任何数据,应该有一个“真相源”。通常,首先将状态添加到需要渲染的组件中。然后,如果其他组件也需要它,可以将它提升到最接近的共同祖先。与其试图在不同组件之间同步状态,您应该依赖自顶向下的数据流。
提升状态涉及编写比双向绑定方法更多的“样板”代码,但作为一个好处,查找和隔离错误需要较少的工作。由于任何状态“存在于”某个组件中,并且该组件本身可以改变它,所以错误的表面积大大降低。另外,您可以实现任何自定义逻辑来拒绝或转换用户输入。
如果某件事可以从道具或状态中推导出来,那么它可能不应该处于这个状态。例如,而不是存储既celsiusValue
和fahrenheitValue
,我们只是存储上次编辑temperature
和scale
。其他输入的值可以始终由render()
方法中的值来计算。这让我们可以清除或应用舍入到其他字段,而不会丢失用户输入的任何精度。
当您在UI中发现错误时,可以使用React Developer Tools检查道具并向上移动树,直到找到负责更新状态的组件。这可以让你追踪错误来源: