此文章是翻译Lifting State Up这篇React(版本v16.2.0)官方文档。
Lifting State Up
经常,几个组件需要响应相同的数据变化。我们建议提升共享状态(lifting the shared state up)到距离它们最近的父组件。让我们看看这是如何运转的。
在这部分,我们将要创建一个温度计算器来计算给定的温度是否使水沸腾。
我们将创建一个BoilingVerdict 组件。它接受一个celsius 温度作为props,然后输出是否能够使水沸腾:
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>The water would boil.</p>;
}
return <p>The water would nott boil.</p>;
}
然后,我们会创建一个Calculator 组件。它渲染一个<input> 接受你的键入的温度( temperature),并将此值保存在this.state.temperature 中。
另外,它会渲染当前输入值BoilingVerdict。
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleChagne = this.handleChagne.bind(this);
this.state = {temperature: ''};
}
handleChagne(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.handleChagne} />
<BoilingVerdict
celsius={parseFloat(temperature)} />
</fieldset>
);
}
}
Adding a Second Input
我们的新需求是,除了Celsius 输入 ,我们还需要提供一个Fahrenheit 输入,并且让它们保持同步。
开始我们可以从Calculator 组件中提取一个TemperatureInput 组件。我们将添加一个scale prop ,只可以接受“c”或“f”:
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
};
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChagne = this.handleChagne.bind(this);
this.state = {temperature: ''};
}
handleChagne(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 去渲染两个不同的temperature 输入:
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
现在我们有两个input,但是当你在其中一个键入temperature时,另一个不会更新。这同我们的需求相反:我们想要它们保持同步。
我们也不能在Calculator 中展示BoilingVerdict。这个Calculator 不知道当前的temperature 因为它被隐藏在TemperatureInput 中。
Writing Conversion Functions
首先,我们要写两个函数来互相转换Celsisus 和 Fahrenheit。
function toCelsius(fahrenheit) {
return (fahrenheit - 32 ) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
这两个函数转换数值。我们还要写另一个函数,它接受一个string 类型的value和一个转换函数作为参数并且返回一个string。我们调用此函数来通过一个输入值获取另一个输入值。
如果是一个无效的temperature 它将返回一个空字符串,并且输出值保留三位小数:
function tryCovert(value, convert) {
const input = parseFloat(value);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
}
例如,tryConvert('abc', toCelsisu) 将返回一个空字符串,而tryConvert('10.22', toFahrenheit) 将返回50.369。
Lifting State Up
当前,两个TemperatureInput 组件各自独立保存它们的值在本地state 中:
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;
// ...
但是,我们希望这两个输入是相互同步的。 当我们更新Celsisus 输入时,Fahrenheit 输入应反映转换的温度,反之亦然。
在React 中,共享状态是通过将其移动到需要它的组件的最接近的共同祖先来实现的。 这被称为“提升状态(lifting state up)”。 我们将从TemperatureInput 中删除本地state,并将其移动到Calculator 中。
如果Calculator 拥有共享state,则它将成为两个输入中当前温度的“真实来源(source of truth)”。 它可以指示他们具有彼此一致的值。 由于两个TemperatureInput 组件的props来自同一个父Calculator 组件,所以两个输入将始终保持同步。
让我们看看这是如何一步一步工作的。
首先,我们将在TemperatureInput 组件中用this.props.temperature 替换this.state.temperature。 现在,假设this.props.temperature 已经存在,虽然我们将来需要从Calculator 传递它:
render() {
// Before: const temperature = this.state.temperature
const temperature = this.props.temperature;
// ...
}
我们知道props 是只读的。当temperature 处于本地state 时,TemperatureInput 可以调用this.setState() 来更改它。 然而,现在temperature 作为prop 来自父组件,TemperatureInput 无法控制它。
在React 中,通常通过使组件“可控的(controlled)”来解决。 就像DOM <input>一样,同时接受一个value 和一个onChange prop,所以自定义的TemperatureInput 也可以接受来自其父Calculator 的temperature 和onTemperatureChange props。
现在,当TemperatureInput想要更新其temperature 时,它会调用this.props.onTemperatureChange:
handleChange(e) {
// Before: this.setState({temperature: e.target.value})
this.props.onTemperatureChange(e.target.value);
}
注意:
自定义组件中的
temperature或onTemperatureChangeprop名称没有特殊的含义。 我们可以叫他们任何其他的东西,像命名他们为value和onChange这是一个常见的约定。
onTemperatureChange prop将与temperature prop一起由父Calculator 组件提供。 它将通过修改自己的本地state来处理改变,从而使用新值重新渲染将两个输入。 我们将很快看到新的Calculator 实现。
在深入到Calculator 进行更改之前,让我们回顾一下对TemperatureInput 组件的更改。 我们已经从中删除了本地state,而不是读取this.state.temperature,我们现在读取this.props.temperature。 我们现在调用this.props.onTemperatureChange(),而不是调用this.setState() ,这将由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存储在本地state。 这是我们从inputs中“提升(lifted up)”的state,它们将作为他们两个的“真理之源(source of truth)”。 为了渲染两个输入,我们需要知道的所有数据的最小表示。
例如,如果我们在摄氏度输入中输入37,则Calculator 组件的state将是:
{
temperature: '37',
scale: 'c'
}
如果我们稍后将Fahrenheit 字段编辑为212,则Calculator 的state将为:
{
temperature: '212',
scale: 'f'
}
我们可以存储了两个输入的值,但是这么做是不必的。保存最近改变的输入的值,和它表示的单位就足够了。我们可以基于当前的temperature 和scale 推测出另一个输入的值。
我们可以存储两个inputs 的值,但实际上是不必要的。 存储最近更改的input 的值以及它所代表的scale 就足够了。 然后,我们可以基于当前的temperature 和scale 来推断另一个input 的值。
输入保持同步,因为它们的值是从相同state 计算的:
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' ? tryCovert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryCovert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={this.handleCelsiusChange}/>
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={this.handleFahrenheitChange}/>
<BoilingVerdict
celsisu={parseFloat(celsisu)} />
</div>
);
}
}
现在,无论是你在那一个输入进行编辑,在Calculator 中this.state.temperature 和this.state.scale 都会更新。其中的一个输入获取value,任何用户输入都会被保存,另一个输入都会基于这个值来更新。
让我们回顾一下编辑输入时会发生什么:
- React 调用在DOM
<input>上指定为onChange的函数。在我们的例子中,这是TemperatureInput组件中的handleChange方法。 TemperatureInput组件中的handleChange方法使用新的所需值调用this.props.onTemperatureChange()。其props,包括onTemperatureChange,由其父组件Calculator提供。- 当它渲染之前,
Calculator已经指定了CelsiusTemperatureInput的onTemperatureChange是Calculator的handleCelsiusChange方法,而FahrenheitTemperatureInput的onTemperatureChange是Calculator的handleFahrenheitChange方法。因此,根据我们编辑的input,调用这两个Calculator方法。 - 在这些方法中,
Calculator组件要求React 通过使用新的输入值和刚刚编辑的输入的当前scale 调用this.setState()来重新渲染自身。 - React 调用
Calculator组件的render方法来了解UI的外观。基于当前temperature和有效scale 重新计算两个inputs的值。这里进行温度转换。 - React 使用
Calculator指定的新props 调用各个TemperatureInput组件的渲染方法。它会了解UI的外观。 - React DOM更新DOM以匹配所需的输入值。我们刚刚编辑的输入接收其当前值,另一个输入更新为转换后的temperature。
每个更新都会执行相同的步骤,以便输入保持同步。
Lessions Learned
在React 应用中任何数据变动都应该是基于一个"source of truth"。通常state 被首先加入到需要它去渲染的组件。然后,如果另一个组件也需要它,那么你就可以将它提升到它们的父组件中。而不是尝试在不同的组件中同步state,你应该依赖top-down data flow。
提升state 专注写模版代码(boilerplater)而不是进行双向绑定,但是有一个优点,它能花费很少工作去发现和隔离bug。因为任何state 都“存在(lives)”于组件而组件可以独立改变state,确定bug 的范围也会减少。除此之外,你可以实现自定义逻辑去拒绝或改变用户输入。
如果有些东西可以来自props 或者state ,那么它就不应该存在与state 中。例如,我们存储上一次编辑的temperature 和scale 而不是存储celsiusValue 和fahrenheitValue。另一个输入值总是可以在render() 方法中被计算得到。这可以让我们清晰的 应用在任何取值范围而不会失去用户输入精度。
如果在UI 中有错误,你可以使用React Developer Tools 对props 进行检测,并将其移动到树上,知道找到负责更新state 的组件为止。这可以让你从源代码上跟踪bug。
