title | date | version |
---|---|---|
快速起步-状态提升 |
2017-04-25 10:27:42 -0700 |
15.5.0 |
通常,多个组件会响应同样的变化数据。我们建议将这种共享的状态提升到最近的共同的祖先。让我们看看它是如何工作的。
在这一节中,我们会创建一个温度计算器用来计算水是否在给定的温度下沸腾。
我们先创建一个 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
(计算器)的组件。它会渲染一个可让你输入摄氏温度的输入框,并将该输入框的值绑定到 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>
);
}
}
我们的新需求是,我们不仅要提供摄氏温度输入,还需要提供华氏温度输入,并要自动进行转换。
我们从 Calculator
提取出一个叫 TemperatureInput
的组件,并运行传入 "c"
或者 "f"
给它的属性 scale
:
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>
);
}
}
现在,我们有两个输入框了,但是当我们在其中一个输入值时,另外一个并不会更新。这违背了我们的需求,我们想让它们保持同步。
此时,我们也不能从 Calculator
中展示 BoilingVerdict
。 因为 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
拥有共享状态,它就成为了多个温度输入组件的的 “source of truth”(译者理解:唯一来源)。它可以让输入组件具有彼此一致的值。自从将 TemperatureInput
组件的属性提取到共同的父组件 Calculator
后,它们的输入值会总是同步的。
我们看看看它是如何一步步开始工作的。
首先,我们会在 TemperatureInput
组件中,使用 this.props.temperature
来替换 this.state.temperature
。现在,先假设 this.props.temperature
总是存在的。将来,我们会在 Calculator
组件中传递给它:
render() {
// Before: const temperature = this.state.temperature;
const temperature = this.props.temperature;
我们知道 属性是只读的。当 temperature
是本地状态时,TemperatureInput
组件通过 this.setState()
来改变它。然而,temperature
成为了父组件的属性,TemperatureInput
组件就没有该属性的控制权了。
在 React
中,通常让组件 controlled
来解决该问题。就像DOM中的 <input>
接收一个 value
和 onChange
属性,自定义的 TemperatureInput
会从 Calculator
(父组件)中接收 temperature
和 onTemperatureChange
。
现在,当 TemperatureInput
想更新它的温度,可以调用 this.props.onTemperatureChange
:
handleChange(e) {
// Before: this.setState({temperature: e.target.value});
this.props.onTemperatureChange(e.target.value);
注意自定义组件中的 temperature
和 onTemperatureChange
没有特殊的含义。我们也可以用其他名称代替,比如 value
和 onChange
这种常用的惯例。
组件中的 onTemperatureChange
属性将会通知父组件 Calculator
温度变化。父组件将通过修改自己的本地状态来处理更新,并为两个输入组件提供新的值。我们将很快看到新的 Calculator
实现。
在更新 Calculator
之前,我们先更新 TemperatureInput
。先移除它的本地状态,使用 this.props.temperature
来替代this.state.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 (
<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>
);
}
}
现在,无论你是在哪个温度输入组件中输入,Calculator
组件中的 this.state.temperature
和 this.state.scale
都会更新。其中一个输入值,另一个的输入值总是基于它重新计算,所以随便输入哪个,值都会生效。
让我们回顾一下编辑输入框时会发生什么:
- React会调用DOM元素
<input>
上的onChange
函数。在我们的示例中, 是TemperatureInput
组件的handleChange
方法。 TemperatureInput
组件的handleChange
方法会调用this.props.onTemperatureChange()
并传入新的期望值。这是由父组件Calculator
传递来的属性。- 在渲染呈现之前,如果在
TemperatureInput
组件中输入摄氏温度,onTemperatureChange
将会调用父组件的handleCelsiusChange
方法,如果是输入的华氏温度,onTemperatureChange
将会调用父组件的handleFahrenheitChange
方法。 因此,会根据具体的输入,调用这两个方法中的一个。 - 在这些方法内部,
Calculator
组件通过this.setState()
通知React
使用最新的输入值来重新渲染它自己。 - React 调用
Calculator
组件的render
方法来确定需要展示的UI。所有输入框的值,都将通过当前温度和温度类型来重新计算。温度就在这个阶段进行转换的。 - 当
Calculator
传递给TemperatureInput
的属性发生变化时,React调用TemperatureInput
组件的render()
方法来确定如何渲染。 - 最终,React DOM 使用期望的输入值,来更新DOM。在我们更新一个的时候,另一个输入框也就同步更新。
每个更新都会执行相同的步骤,以便输入保持同步。
对于在 React
中更改的任何数据,应该有一个唯一的来源。通常,状态首先会被添加到使用它的组件中。然后,当其他组件也需要它的时候,可以将它提升到最近的公共祖先上,而不是尝试同步这些状态。我们应该使用 单向数据流。
提升状态会比双向绑定编写更多的样板代码,但它有一个好处,不容易制造bug。提升状态到单一组件后,仅有该组件可以修改它,这导致错误的可能性大大降低。此外,您可以实现任何自定义的逻辑来拒绝或者转换用户的输入。
如果一些东西可以通过 props
或者 state
得到,那么他可能并不需要放在共享状态中。例如,我们并没有存储 celsiusValue
和 fahrenheitValue
,而是存储最后编辑的 temperature
和 scale
。其他的值总是可以在 render()
中被计算出来。这使得我们可以清理或者四舍五入到其他字段,而不会在用户输入中丢失精度。
当你在UI中看到错误时,您可以使用 React Developer Tools 来检查属性,并向上查找组件树直至到负责更新的组件上。这可以让你跟踪这些错误来源: