经常有些组件需要映射同一个改变的数据。我们建议将共用的state提升至最近的同一个祖先元素。我们来看看这是怎样运作的。
在这一节中,我们会创建一个温度计算器来计算提供的水温是否足够沸腾。
我们先创建一个叫BoilingVerdict的组件。它接受摄氏度温度为prop,并且打印水温是否足够沸腾:
function BoilingVerdict(props) { if (props.celsius >= 100) { returnThe water would boil.
; } returnThe water would not boil.
;}
下一步,我们创建一个Calculator组件。它渲染一个<input>让你输入温度,然后将温度保存在this.state.value里。
另外,它通过当前输入的温度值渲染BoilingVerdict组件。
class Calculator extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.state = {value: ''}; } handleChange(e) { this.setState({value: e.target.value}); } render() { const value = this.state.value; return (); }}
。
添加第二个输入框
我们新的要求是除了摄氏度的输入之外,我们还提供一个华氏度输入框,并且它们保持同步。
我们可以先从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 = {value: ''}; } handleChange(e) { this.setState({value: e.target.value}); } render() { const value = this.state.value; const scale = this.props.scale; return (); }}
现在来修改Calculator组件来渲染两种单独的温度输入:
class Calculator extends React.Component { render() { return (); }}
。
现在我们有两种输入了,但是当你在其中一个输入温度后,另外一个却不会更新。这和我们的要求矛盾了:我们想让它们同步。
我们也不能在Calculator里显示BoilingVerdict。Calculator不知道当前的温度因为温度被隐藏在了TemperatureInput里。
写转换函数
首先,我们会写两个函数去将摄氏度和华氏度互相转换:
function toCelsius(fahrenheit) { return (fahrenheit - 32) * 5 / 9;}function toFahrenheit(celsius) { return (celsius * 9 / 5) + 32;}
这两个函数转换数字。我们会写另外一个函数,它接收一个字符串值和一个转换函数做为参数并且返回一个字符串。我们用这个函数来根据一个输入来计算另一个输入。
如果最终结果无效它会返回一个空字符串,并且它会将结果四舍五入保存到小数点后第三位:
function tryConvert(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', toCelsius)返回一个空字符串,tryConvert('10.22', toFahrenheit)返回'50.396'。
提升state
现在,两个TemperatureInput组件都独立地将它们的值保存在本地state里:
class TemperatureInput extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.state = {value: ''}; } handleChange(e) { this.setState({value: e.target.value}); } render() { const value = this.state.value;
然而,我们希望这两个输入彼此之间可以同步。当我们更新了摄氏度输入,华氏度输入应该反映出转换后的温度,反之亦然。
在React里,共用的state通过把它传递给组件里最近的共同的祖先来完成。这一步骤叫做“提升state”。我们会从Temperature移除本地state并且把它移到Calculator里。
如果Calcutor拥有了共用的state,它变成了当前两个温度输入的“真正数据源”。它可以指示它们两个都拥有值并且彼此一致。自从两个TemperatureInput组件的props都来自于同一个父亲Calculator组件,那么这两个输入会一直保持同步。
让我们一步一步来看看这是如何运作的。
首先,在temperatureInput组件里,我们会用this.props.value替换this.state.value。目前,让我们假装this.props.value已经存在了,虽然我们在后面将需要把它从Calculator里传递过来:
render() { // Before: const value = this.state.value; const value = this.props.value;
我们知道props是只读的。当value在state里,TemperatureInput能够调用this.setState()来改变它。然而,现在value是从父组件传递来并且作为一个prop,TemperatureInput组件无法控制它。
在React里,这样的情况经常通过使组件“受控”来解决。就像<input>DOM元素同时接受一个value和一个onChange属性,因此自定义的TemperatureInput也能从它的父组件Calculator那里同时接受value和onChange。
现在,当TemperatureInput想要更新温度,它就会调用this.props.onChange:
handleChange(e) { // Before: this.setState({value: e.target.value}); this.props.onChange(e.target.value);
注意自定义组件里的value和onChange属性都没有特殊的含义。虽然这是一个共同的约定,但是我们可以给它们取任意名字。
onChange属性和value属性会一起通过父组件Calculator提供。它将会处理修改它自己的本地state的变化,从而使用新的值重新渲染两个输入。我们将很快看到Calculator的完成。
在深入Calculator组件里的变化之前,让我们先简要重述一下Temperature组件的变化。我们从本地state里移除了它,并且现在读取this.props.value而不是this.state.value。当我们要改变的时候,我们不调用this.setState(),而是调用Calsulator组件提供的this.props.onChange():
class TemperatureInput extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); } handleChange(e) { this.props.onChange(e.target.value); } render() { const value = this.props.value; const scale = this.props.scale; return (); }}
现在让我们看一看Calculator组件。
我们将要保存当前输入的value和scale在它的state里。这是我们从输入那里提升的state,并且它会像真正源一样为两种输入服务。这是所有我们需要了解的为了渲染两个输入的数据的最小限度的表示。
举个例子,如果我们输入37到摄氏度输入框里,Calculator组件的state会像这样:
{ value: '37', scale: 'c'}
{ value: '212', scale: 'f'}
我们本应该保存两个输入但是结果发现这不重要了。保存最近改变的输入和它代表的比例单位已经足够了。然后我们可以根据当前的value和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 = {value: '', scale: 'c'}; } handleCelsiusChange(value) { this.setState({scale: 'c', value}); } handleFahrenheitChange(value) { this.setState({scale: 'f', value}); } render() { const scale = this.state.scale; const value = this.state.value; const celsius = scale === 'f' ? tryConvert(value, toCelsius) : value; const fahrenheit = scale === 'c' ? tryConvert(value, toFahrenheit) : value; return (); }}
。
现在,不论你编辑哪一个输入框,Calculator组件里的this.state.value和this.state.scale都会更新。其中获得value的输入保持现状,因此所有用户的输入都被保存,然后另外一个输入的值由已输入的值计算出。
我们简述当编辑一个输入框的时候会发生什么:
- React在DOM元素<input>上调用指定的onChange函数。在我们这个例子下,调用的是TemperatureInput组件的handleChange方法。
- TemperatureInput组件的handleChange方法调用this.props.onChange()带着新的value。它的属性,包括onChange都是父组件Calculator提供的。
- 当它先前渲染的时候,Calculator组件指定摄氏度TemperatureInput组件的onChange就是Calculator组件的handleCelsiusChange方法,并且华氏度TemperatureInput组件的onChange就是Calculator的handleFahrehnheitChange方法。因此这两个Calculator方法被调用取决于哪一个输入框我们编辑了。
- 在这些方法内部,Calculator组件会要求React去重新渲染它自己通过调用this.setState(),传递的值是新输入的温度和当前的比例单位。
- React调用Calculator组件的render方法得知UI应该是什么样子。两个输入框的值都依据当前温度和比例单位重新计算。温度转换就在这里完成。
- React调用单独的TemperatureInput的render方法,传递Calculator指定的新props。它会得知UI的样子。
- React DOM更新UI来匹配输入的值。我们编辑的值会保持原状,另外一个值会通过计算得出,每一个更新都经过同样的步骤所以输入会彼此同步。
学习总结
在一个React应用里应该有一个“单一数据源”来应对任何改变的数据。通常,state是首先被添加到组件里需要它来处理渲染。接着,如果其他组件也需要state,你可以把它提升到最近的共同的祖先组件。想要在不同的组件之间同步state,你需要依赖从上到下的数据流。
将state提升会牵扯到写更多的“引用”代码,相比双向绑定要多得多。但是作为一个好处就是,这样子可以更快的找到和隔离程序中的bug。因为哪个组件保有状态数据,也只有它自己能够操作这些数据,发生bug的范围就被大大地减小了。此外,你可以完成任何自定义的逻辑去丢弃或者转变用户输入。
如果有东西可以既从props也可以从state里获取到,那么它可能不应该出现在state里。举个例子,我们只保存下最后一次编辑的value和scale,而不是去保存celsiusValue和fahrenheitValue的值。另一个输入框的值可以在render()方法里通过计算算出。这样我们可以清除或者运用四舍五入为其他输入域而不丢失精确度。
当你看到UI里有某些东西发生错误了,你可以使用去检查props并且顺着组件树寻找直到你找到更新state的组件。这样你可以跟踪bug到它们的源头。