状态提升
通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。让我们看看它是如何运作的。创建一个用于计算水在给定温度下是否会沸腾的温度计算器。
将从一个名为 BoilingVerdict 的组件开始,它接受 celsius 温度作为一个 prop,并据此打印出该温度是否足以将水煮沸的结果。
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>
);
}
}
添加第二个输入框
在已有摄氏温度输入框的基础上,我们提供华氏度的输入框,并保持两个输入框的数据同步。
先从 Calculator 组件中抽离出 TemperatureInput 组件,然后为其添加一个新的 scale prop,它可以是 "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>
)
}
}
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
ReactDOM.render(
<Calculator />,
document.getElementById('root')
)
编写转换函数
首先,我们将编写两个可以在摄氏度与华氏度之间相互转换的函数:
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();
}
状态提升
在 React 中,将多个组件中需要共享的 state 向上移动到它们的最近共同父组件中,便可实现共享 state。这就是所谓的“状态提升”。接下来,我们将 TemperatureInput 组件中的 state 移动至 Calculator 组件中去。
如果 Calculator 组件拥有了共享的 state,它将成为两个温度输入框中当前温度的“数据源”。它能够使得两个温度输入框的数值彼此保持一致。由于两个 TemperatureInput 组件的 props 均来自共同的父组件 Calculator,因此两个输入框中的内容将始终保持一致。
首先,我们将 TemperatureInput 组件中的 this.state.temperature 替换为 this.props.temperature。现在,我们先假定 this.props.temperature 已经存在,尽管将来我们需要通过 Calculator 组件将其传入:
render() {
const temperature = this.props.temperature;
当 temperature 存在于 TemperatureInput 组件的 state 中时,组件调用 this.setState() 便可修改它。然而,temperature 是由父组件传入的 prop,TemperatureInput 组件便失去了对它的控制权。
在 React 中,这个问题通常是通过使用“受控组件”来解决的。与 DOM 中的 <input> 接受 value 和 onChange 一样,自定义的 TemperatureInput 组件接受 temperature 和 onTemperatureChange 这两个来自父组件 Calculator 的 props。
现在,当 TemperatureInput 组件想更新温度时,需调用 this.props.onTemperatureChange 来更新它:
handleChange(e) {
this.props.onTemperatureChange(e.target.value);
我们想要响应数据改变时,我们需要调用 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>
);
}
}
我们可以存储两个输入框中的值,但这并不是必要的。我们只需要存储最近修改的温度及其计量单位即可,根据当前的 temperature 和 scale 就可以计算出另一个输入框的值。
由于两个输入框中的数值由同一个 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' ? 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>
);
}
}
