Reactjs入门官方文档(九)【lifting-state-up】
此文章是翻译lifting-state-up这篇React(版本v15.5.4)官方文档。
状态提升(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>
}else{
return <p>The water would nott boil.</p>
}
}
然后,我们会创建一个Calculator
组件。它渲染一个<input>
接受你的键入的温度( temperature),并将此值保存在this.state.temperature
中。
另外,它会当前输入值渲染BoilingVerdict
。
class Calculator extends Component {
constructor(props){
super(props)
this.state = {
temperature: ''
}
this.handleChagne = this.handleChagne.bind(this)
}
handleChagne(event){
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.handleChagne} />
<BoilingVerdict celsius={parseFloat(temperature)} />
</fieldset>
)
}
}
添加第二个输入
我们的新需求是,除了Celsius input ,我们还需要提供一个Fahrenheit input,并且让它们保持同步(in sync)。
开始我们可以从Calculator
组件中提取一个TemperatureInput
组件。我们将添加一个scale
prop ,只可以接受“c”
或“f”
:
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
}
class TemperatureInput extends Component {
constructor(props){
super(props)
this.state = {
temperature: ''
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(event){
this.setState({
temperature: event.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 inputs:
class Calculator extends Component {
render(){
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
)
}
}
现在我们有两个input,但是当你在其中一个键入temperature时,另一个不会更新。这同我们的需求相反:我们想要它们保持同步(in sync)。
我们也不能在Calculator
中展示BoilingVerdict
。这个Calculator
不知道当前的temperature 因为它被隐藏在TemperatureInput
中。
编写转换函数
首先,我们要写两个函数来互相转换Celsisus
和 Fahrenheit
。
function toCelsius(fahrenheit){
return (fahrenheit - 32 ) * 5 / 9
}
function toFahrenheit(celsius){
return (celsius * 9 / 5) + 32
}
这两个函数转换数值。我们还要写另一个函数,它接受一个string 类型的value
和一个转换函数作为参数并且返回一个string。我们调用此函数来通过一个输入值获取另一个输入值。
如果是一个无效的value
它将返回一个空字符串,并且输出值保留三位小数。
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 input时,Fahrenheit input 应反映转换温度,反之亦然。
在React中,共享状态是通过将其移动到需要它的组件的最接近的共同祖先来实现的。 这被称为“提升状态(lifting state up)”。 我们将从TemperatureInput
中删除本地状态,并将其移动到Calculator
中。
如果Calculator
拥有共享状态,则它将成为两个输入中当前温度的“真实来源(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
或onTemperatureChange
prop名称没有特殊的含义。 我们可以叫他们任何其他的东西,像命名他们为value
和onChange
这是一个常见的惯例。
onTemperatureChange
prop将与父Calculator
组件的temperature
prop一起提供。 它将通过修改自己的本地state来处理change,从而将两个输入重新提供给新值。 我们将很快看到新的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'
}
我们可以存储两个inputs 的值,但实际上是不必要的。 存储最近更改的input 的值以及它所代表的scale 就足够了。 然后,我们可以基于当前的temperature
和scale
来推断另一个input 的值。
inputs 保持同步,因为它们的值是从相同state计算的:
class Calculator extends 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>
)
}
}
现在,无论是你在那一个input 进行编辑,在Calculator
中this.state.temperature
和this.state.scale
都会更新。其中的一个input 获取value,任何input 都会被保存,另一个input 都会基于这个值来更新。
让我们回顾一下编辑input时会发生什么:
- 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通过使用新的input 值和刚刚编辑的input的当前scale 调用this.setState()
来重新渲染自身。 - React调用
Calculator
组件的render
方法来了解UI的外观。基于当前temperature和有效scale重新计算两个inputs的值。这里进行温度转换。 - React使用
Calculator
指定的新props调用各个TemperatureInput
组件的渲染方法。它会了解UI的外观。 - React DOM更新DOM以匹配所需的输入值。我们刚刚编辑的input 接收其当前值,另一个input 更新为转换后的temperature。
每个更新都会执行相同的步骤,以便输入保持同步。
经验总结
在React 应用中任何数据变动都应该是基于一个“单一数据源””source of truth”。通常state 被首先加入组件去渲染。然后,如果另一个组件也需要它,那么你就可以将它们提升到它们的父组件中。你应该依赖从上到下的数据流向top-down data flow,而不是尝试在不同的组件中同步state。
通过专注写模版代码(boilerplater)而不是进行双向绑定,但是有一个优点,它能花费很少工作去发现和隔离bug。因为任何state 都“存在(lives)”于组件而组件可以独立改变state,确定bug 的范围也会减少。除此之外,你可以实现自定义逻辑去拒绝或改变用户输入。
如果有些东西可以来自props 或者state ,那么它就不应该存在与state 中。例如,我们存储temperature
和scale
而不是存储celsiusValue
和fahrenheitValue
。另一个input 值总是可以在render()
方法中被计算得到。这可以让我们清晰的 应用在任何取值范围而不会失去用户输入精度。
如果在UI 中有错误,你可以使用React Developer Tools 对props 进行检测,并向上遍历树,直到找到负责更新状态的组件。这使你可以跟踪到 bug 的源头:
上一篇: JS数值类型数组去重
下一篇: php能做应用吗