React组件State提升(译)
译自:https://reactjs.org/docs/lifting-state-up.html (适当进行了裁减)
通常我们会碰到这样的情况,当某个组件的state数据改变时,几个React组件同时都需要做出反应。这时我们推荐把相应的state值共享到这些组件最接近的父类中。让我们看下实际是怎么做的。
在这个章节,我们将创建一个“温度计算器”,这个计算器通过给定的温度来计算水是不是沸腾了。
我们将从一个BoilingVerdice(沸水判定)组件开始。这个组件接受一个摄氏温度值作为其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中。
此外,Calculator组件还将这个温度值传给它的子组件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> ); } }
我们现在可以改变Calculator组件来呈现两个不同的温度输入:
class Calculator extends React.Component { render() { return ( <div> <TemperatureInput scale="c" /> <TemperatureInput scale="f" /> </div> ); } }
现在我们有了两个温度输入框,但目前两入输入框不会同步,当我们改动一个输入框的温度值时,另一个输入框的值不会随着改变。这个没有达到我们的需求:我们希望这两个输入框中的值能保持同步变化。
并且在当前这个Calculator组件中我们也无法显示BoilingVerdict组件的内容。因为当前的温度值隐藏在TemperatureInput组件内部了,使得Calculator组件无法获得当前的温度值。
编写转换函数
首先,我们将编写两个函数,将摄氏度转换为华氏温度,然后返回:
function toCelsius(fahrenheit) { return (fahrenheit - 32) * 5 / 9; } function toFahrenheit(celsius) { return (celsius * 9 / 5) + 32; }
这两个函数负责转换数值。我们将编写另一个函数,它接受一个字符串温度和一个转换器函数作为参数,并返回一个字符串。我们将使用它根据一个输入框的温度值计算一个输入框的温度值。
当输入是无效的温度值时这个函数返回一个空字符串,并将输出保留到小数点后的第三位:
function tryConvert(temperature, convert) { const input = parseFloat(temperature); if (Number.isNaN(input)) { return ''; } const output = convert(input); //Math.round()是取整四舍五入 const rounded = Math.round(output * 1000) / 1000; return rounded.toString(); }
例如,tryConvert('abc'、to摄氏度)返回一个空字符串,而tryConvert('10.22'、toFahrenheit)返回'50.396'。
提升State
目前,两个TemperatureInput组件都各自保持输入的温度值在自己的local 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; // ...
但是,我们希望这两个输入是同步的。当我们更新摄氏输入时,华氏输入应反映转换后的温度,反之亦然。
在React中,共享状态通过将需要共享的state值移动到需要它的组件的最近的共同祖先来完成。这被称为“提升State”。我们将state.temperature这个值从TemperatureInput中移除,并将其放入Calculator组件中。
如果Calculator组件拥有了共享状态,它就将成为当前两个温度输入的“真实来源”。这样就可以通过一定的代码引导使得两个温度输入子组件的值保持一致。由于这两个温度输入子组件的props都来自同一个父计算器组件,所以这两个输入将可以做到同步了。
让我们看一下详细的步骤:
我们将把TemperatureInput组件中的this.state.temperature替换为this.props.temperature。现在,让我们假设this.props.temperature已经存在,尽管将来我们需要从Calculator组件上传递它:
render() { // Before: const temperature = this.state.temperature; const temperature = this.props.temperature; // ...
我们知道props是只读的。在TemperatureInput组件中,这个温度值是保存在local state中,所以只能通过调this.setState()来改变它。然而,现在这个温度值是来自父组件的prop,TemperatureInput组件将无法控制这个温度值的修改。
在React中,通常通过让组件“受控”来解决这个问题。就像DOM <input>同时接受一个值和一个onChange props一样,TemperatureInput组件可以从它的父组件中接受温度值和onTemperatureChange函数做为温度变化的props。
现在,当TemperatureInput组件想要更新它的温度值,只要调this.props.onTemperatureChange就可以了。
handleChange(e) { // Before: this.setState({temperature: e.target.value}); this.props.onTemperatureChange(e.target.value); // ...
在深入研究计算器中的更改之前,让我们回顾一下对TemperatureInput组件的更改。this.state.temperature已经从local state中移除,通过this.props.temperature来替代。当我们要进行更改时,我们不再调用this.setState(),而是调用Calculator组件提供的this.props.onTemperatureChange() :
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组件,我们将存储当前输入的温度值和刻度在local state中。这是我们从输入组件中“提升”的state,它将成为两者的“真实之源”。 它是我们需要知道的所有数据的最小表示,以便两个输入组件的呈现,完整的代码如下:
const scaleNames = { c: 'Celsius', f: 'Fahrenheit' }; function toCelsius(fahrenheit) { return (fahrenheit - 32) * 5 / 9; } function toFahrenheit(celsius) { return (celsius * 9 / 5) + 32; } 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(); } function BoilingVerdict(props) { if (props.celsius >= 100) { return <p>The water would boil.</p>; } return <p>The water would not boil.</p>; } 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> ); } } 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> ); } } ReactDOM.render( <Calculator />, document.getElementById('root') );
界面如下图:
现在就可以达到我们想要的效果了,两个输入框无论改变哪一个,另一个也会随着改变。其中一个温度值是用户的输入,我们会在组件内部保存下来,另一个温度值会通过计算得到。
对于在React应用程序中发生更改的任何数据,都应该有一个“真实来源”。通常,state首先被添加到需要它进行呈现的组件中。然后,如果其他组件也需要它,您可以将它提升到它们最近的共同祖先。与尝试在不同组件之间同步状态不同,您应该依赖自顶向下的数据流。