状态提升
通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中区,让我们看看它是如何运作的。
在本节中,我们将穿件一个用于计算水在给定温度下是否会沸腾的温度计算器。
我们将从一个名为boilingVerdict的组件开始,他接受celsius温度座位一个prop,并据此打印出该温度是否足以将水煮沸的结果。
function BoilingVerdict (props) {
if (props.celsius >= 100) {
return <p>The water would boil.</p>
} else {
return <p>The water would not boil.</p>
}
}接下来,我们创建一个名为Calculator的组件。它渲染一个用于输入温度的input,并将其值保存在this.state.temperature中。 另外,它根据当前输入值渲染BolingVerdict组件。
class Calculator extends React.Component {
constructor(props) {
super(props)
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 type="text" value={temperature} onChange={this.handleChange} />
<BoilingVerdict celsius = { parseFloat(temperature)} />
</fieldset>
)
}
}
ReactDOM.render(<Calculator />,document.querySelector("#root"))添加第二个输入框
在已有的摄氏度温度输入框的基础上,我们提供华氏度的输入框,并保持两个输入框的数据同步。 我们先从Calculator组件中抽离出TemperatureInput组件,然后为其添加一个scaleprop,他可以是"c"或是"f":
const scaleNames = {
c: "Celsius",
f: "Fahrenheit"
}
class TemperatureInput extends React.Component {
constructor(props) {
super(props)
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 {
constructor(props) {
super(props)
}
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
ReactDOM.render(<Calculator />, document.querySelector("#root"))我们现在有了两个输入框,但当你在其中一个输入温度时,另一个不会更新。这与我们的要求相矛盾:我们希望让它们保持同步。 另外,我们也不能通过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();
}状态提升
到目前为止,两个TemperatureInput组件均在各自内部的state中相互独立的保存着各自的数据。然而我们希望两个数据框中的数值彼此能够相互同步。当我们更新摄氏度输入框的数值的时候,华氏度输入框应该显示转化后的华氏温度,反之亦然。 在React中,将多个组件中需要共享的state向上移动到他们最近共同的父组件中,便可以实现共享state。这就是所谓的"状态提升"。接下来我们将 TemperatureInput组件中的state移动到Calculator组件中去。
如果Calcilator组件拥有了共享的state,它将成为两个温度输入框中当前温度的"数据源"。他能够是的两个温度输入框的数值彼此保持一致。由于两个TemperatureInput组件中的props均来自共同的组件Calculator,因此两个输入框中的内容始终保持一致。
首先,将自身状态更新为传入的props值:
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>
)
}我们知道props是只读的。当temperature存在于TemperatureInput的组件的state中时,组件通过调用this.setState()便可修改他,但是temperature是由父组件传入的pro p,TemperatureInput组件变失去了对他的控制权。
在React中,这个问题通常是通过使用"受控组件"来解决的。与DOM中的<input>接受value和onChange一样,自定义的TemperatureInput组件接受temperature和onTemperatureChange这两个来自父组件Calculator和props。
现在,当TemperatureInput组件想更新温度时,需要调用this.props.onTemperatureChange来更新它。
handleChange = e => {
// this.setState({
// temperature: e.target.value
// })
this.props.onTemperatureChange(e.target.value)
}onTemperatureChange和temperature就是普通的两个prop的名称,你也可以使用其他的命名。这两者都由父组件提供,通过这个来修改父组件自身内部的state来处理数据的变化,进而使用新的数值重新渲染两个输入框。我们将很快看到修改后的Calculator组件效果。
再看看Calculator组件我们怎么处理,我们会把当前输入的temperature和scale保存在组件内部的state中,这个state就是从两个输入框组件中提升而来的,并它将用作两个输入框组件的共同"数据源"。这是我们为了渲染两个输入框所需要的的所有数据的最小表示。 例如,当我们在摄氏度输入框中输入37的时候,Calculator组件中state将会是:
{
temperature:'37',
scale:'c'
}由于输入框中的数值是由同一个state计算而来的,因此他们始终保持同步,所以我们只需要保存一个state就行:
class Calculator extends React.Component {
constructor(props) {
super(props)
this.state = {
temperature: "",
scale: ""
}
}
onTemperatureChange = (value, scale) => {
this.setState({
temperature: value,
scale: scale
})
}
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" onTemperatureChange={this.onTemperatureChange} temperature={celsius} />
<TemperatureInput scale="f" onTemperatureChange={this.onTemperatureChange} temperature={fahrenheit} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</div>
);
}
}现在无论你编辑哪个输入框的内容,Calculator组件中的this.state.temperature和this.state.scale均会被更新。其中一个输入框保留用户的输入并取值,另一个输入框始终基于这个值显示转换后的结果。
让我们来重新梳理一下当你对输入框内容进行编辑时会发生些什么:
- React会调用DOM中
<input>的Onchange方法。在本实例中,他们是TemperatureInput组件的handleChange方法。 - TemperatureInput 组件中的 handleChange 方法会调用 this.props.onTemperatureChange(),并传入新输入的值作为参数。其 props 诸如onTemperatureChange 之类,均由父组件 Calculator 提供。
- 起初渲染时,用于摄氏度输入的子组件 TemperatureInput 中的 onTemperatureChange 方法与 Calculator 组件中的 handleCelsiusChange 方法相同,而,用于华氏度输入的子组件 TemperatureInput 中的 onTemperatureChange 方法与 Calculator 组件中的 handleFahrenheitChange 方法相同。因此,无论哪个输入框被编辑都会调用 Calculator 组件中对应的方法。
- 在这些方法内部,Calculator 组件通过使用新的输入值与当前输入框对应的温度计量单位来调用 this.setState() 进而请求 React 重新渲染自己本身。 React 调用 Calculator 组件的 render 方法得到组件的 UI 呈现。温度转换在这时进行,两个输入框中的数值通过当前输入温度和其计量单位来重新计算获得。
- React 使用 Calculator 组件提供的新 props 分别调用两个 TemperatureInput 子组件的 render 方法来获取子组件的 UI 呈现。
- React 调用 BoilingVerdict 组件的 render 方法,并将摄氏温度值以组件 props 方式传入。
- React DOM 根据输入值匹配水是否沸腾,并将结果更新至 DOM。我们刚刚编辑的输入框接收其当前值,另一个输入框内容更新为转换后的温度值。
得益于每次的更新都经历相同的步骤,两个输入框的内容才能始终保持同步。
小结
在React应用中,任何可变数据应该只有一个相对应的唯一"数据源"。通常,state都是首先添加到需要渲染数据的组件中去。然后如果其他组件也需要这个state,那么你可以将它提升至这些组件最近共同父组件中。你应该依靠这种自上而下的数据流,而不是尝试在不同组件间同步state。 虽然
