React版本:15.4.2
**翻譯:xiyoki **
通常幾個(gè)組件需要響應(yīng)相同的數(shù)據(jù)變化。我們建議將共享狀態(tài)提升到最接近的共同祖先。讓我們看看這是如何工作的。
在本節(jié)中,我們將創(chuàng)建一個(gè)溫度計(jì)算器,用于計(jì)算水是否在給定溫度下沸騰。
我們將從名為BoilingVerdict
的組件開(kāi)始。它接受celsius溫度為props,并打印是否足以將水燒開(kāi):
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>The water would boil.</p>;
}
return <p>The water would not boil.</p>;
}
下一步,我們將創(chuàng)建一個(gè)名為Calculator
的組件。它將渲染一個(gè)<input>
,讓你輸入溫度,并將該值保存在this.state.value
中。
此外,它用當(dāng)前值渲染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 (
<fieldset>
<legend>Enter temperature in Celsius:</legend>
<input
value={value}
onChange={this.handleChange} />
<BoilingVerdict
celsius={parseFloat(value)} />
</fieldset>
);
}
}
Adding a Second Input(增加第二個(gè)輸入)
我們新的要求是:除了攝氏度輸入框,我們還提供華氏度輸入框,并且二者是同步的。
我們把從Calculator
中提取一個(gè)TemperatureInput
組件作為開(kāi)始。我們將為它增加一個(gè)新scale prop,并且這個(gè)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 (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={value}
onChange={this.handleChange} />
</fieldset>
);
}
}
現(xiàn)在,我們可以改變Calculator
來(lái)渲染兩個(gè)獨(dú)立的溫度輸入:
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
現(xiàn)在我們有兩個(gè)輸入框,但當(dāng)你向其中一個(gè)輸入框輸入溫度時(shí),另一個(gè)并不會(huì)更新。這違反了我們的要求:我們希望它們保持同步。
我們也不能從Calculator
中展示BoilingVerdict
。Calculator
也不知道當(dāng)前的溫度,因?yàn)楫?dāng)前溫度被隱藏在了TemperatureInput
內(nèi)部。
Lifting State Up(狀態(tài)提升)
首先,我們將寫(xiě)兩個(gè)函數(shù)來(lái)將攝氏度轉(zhuǎn)換為華氏度,將華氏度轉(zhuǎn)換為攝氏度:
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
這兩個(gè)函數(shù)轉(zhuǎn)換數(shù)字。我們將寫(xiě)另一個(gè)函數(shù),它接受一個(gè)字符串value和一個(gè)轉(zhuǎn)換器函數(shù)作為參數(shù),并返回一個(gè)字符串。我們將使用它來(lái)計(jì)算其中一個(gè)輸入框的值,而該輸入框的值基于另一個(gè)輸入框。
它返回一個(gè)無(wú)效的空字符串value,并且它將輸出四舍五入到三位小數(shù):
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(‘a(chǎn)bc’,toCelsius)
返回一個(gè)空字符串, tryConvert(’10.22’,toFahrenheit)
返回‘50.396’
。
接下來(lái),我們將從TemperatureInput
中刪除狀態(tài)。
相反,TemperatureInput
組件將接受value
和onChange
處理程序作為prop:
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 (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={value}
onChange={this.handleChange} />
</fieldset>
);
}
}
如果幾個(gè)組件需要訪問(wèn)相同的狀態(tài),這標(biāo)志著狀態(tài)應(yīng)該被提升到最接近的共同祖先。在這個(gè)例子中最接近的祖先就是Calculator
。我們將在它的狀態(tài)中存儲(chǔ)當(dāng)前的value
和scale
。
我們可以存儲(chǔ)兩個(gè)輸入的值,但事實(shí)證明這是不必要的。它足以存儲(chǔ)最近被更改的輸入框的值,以及其表示的scale
。然后我們能基于當(dāng)前的value
和scale
,單獨(dú)推斷其他輸入框的值。
輸入的值保存同步,因?yàn)樗鼈兊闹祻南嗤臓顟B(tài)計(jì)算而來(lái)。
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; {/* 對(duì)狀態(tài)value作進(jìn)一步處理*/}
const fahrenheit = scale === 'c' ? tryConvert(value, toFahrenheit) : value;
return (
<div>
<TemperatureInput
scale="c"
value={celsius}
onChange={this.handleCelsiusChange} />
<TemperatureInput
scale="f"
value={fahrenheit}
onChange={this.handleFahrenheitChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</div>
);
}
}
不論你編輯哪個(gè)輸入框,Calculator
中的this.state.value
和this.state.scale
都會(huì)獲得更新。其中一個(gè)輸入框獲取的值為原樣,因此任何用戶的輸入都被保留,另一個(gè)輸入框中的值總是基于它重新計(jì)算。
Lessons Learned (得到的教訓(xùn))
對(duì)于在React應(yīng)用程序中更改的任何數(shù)據(jù),應(yīng)該有一個(gè)單一的‘真實(shí)來(lái)源’。通常,首先將狀態(tài)添加到需要渲染的組件。然后,如果其他組件也需要它,你可以將其提升到最接近的共同祖先。而不是嘗試在不同組件之間同步狀態(tài),你應(yīng)該依賴于自上而下的數(shù)據(jù)流。
提升狀態(tài)涉及編寫(xiě)比雙向綁定方法更多的‘樣板’代碼。但好處是找到和隔離bug需要較少的工作。由于任何狀態(tài)存在于特定的組件中,并且該組件可以單獨(dú)改變它,所以大大減少了錯(cuò)誤的表面積。此外,你可以實(shí)現(xiàn)任何自定義邏輯以拒絕或轉(zhuǎn)換用戶輸入。
如果數(shù)據(jù)可以從props或state派生,那么它就不應(yīng)該在狀態(tài)之中。例如,我們只存儲(chǔ)了最后編輯的value
和scale
,而不是存儲(chǔ)兩個(gè)celsiusValue
和fahrenheitValue
。另一個(gè)輸入的值總是可以從render()
方法中計(jì)算出來(lái)。這允許我們清除或應(yīng)用四舍五入到其他字段,而不會(huì)丟失用戶輸入的任何精度。
當(dāng)你在UI中看到錯(cuò)誤時(shí),可以使用 React Developer Tools 檢查props,并向上移動(dòng)樹(shù),直到找到負(fù)責(zé)更新?tīng)顟B(tài)的組件。這使你可以跟蹤錯(cuò)誤到其來(lái)源:
