通常,幾個組件需要根據同一個數據變化做出響應。我們建議將這個共享的狀態提升到他們最近的一個共用祖先。讓我們看看實際該怎么做。
在這一節,我們將創建一個溫度計算器,用來計算一給定溫度能否讓水沸騰。
我們從名為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>
);
}
}
現在我們可以改變Calculator
來渲染兩個獨立的溫度輸入:
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
在CodePen上試一試
現在我們有兩個輸入了,但是當你在其中一個里輸入溫度,另一個不會去更新。這就不滿足我們的需求了,我們想讓他們同步。
我們也沒在Calculator
中顯示BoilingVerdict
。Calculator
不知道當前的溫度,因為溫度被隱藏在TemperatureInput
中。
編寫轉換函數
首先我們寫兩個函數來互相轉換攝氏度和華氏溫度。
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
這兩個函數轉換數字。接下來我們寫另一個函數,接受一個temperature
字符串和一個轉換函數作為參數,返回一個字符串。我們將用他來根據另一個input來計算這個input的值。
一個無效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();
}
比如,tryConvert('abc', toCelsius)
返回空字符串,tryConvert('10.22', toFahrenheit)
返回'50.396'
。
提升狀態
目前,兩個TemperatureInput
組件都單獨地在本地狀態中保存自己的值:
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;
然而,我們希望這兩個input可以彼此同步。當我們更新攝氏度input,華氏度input也會相應的轉換溫度,反之亦然。
在React中,想要共享狀態,就需要找到共享狀態組件的一個最近共有祖先,然后通過將該狀態移動到這個共有祖先上來完成共享。這稱為“提升狀態”。我們先移除TemperatureInput
的本地狀態,取而代之的是將它移到Calculator
中。
如果Calculator
擁有共享狀態,對于兩個input中的溫度來說他就成為了“真相的來源”。他就可以指示兩個input具有相同的值。由于兩個TemperatureInput
組件的props都來自同一個父組件Calculator
,兩個input將始終保持同步。
讓我們逐步分析這是如何工作的。
首先,在TemperatureInput
組件中,我們使用this.props.temperature
將this.state.temperature
替換掉。現在,讓我們假設this.props.temperature
已經存在,之后我們會從Calculator
中傳入該值:
render() {
// Before: const temperature = this.state.temperature;
const temperature = this.props.temperature;
我們知道props是只讀的。之前temperature
在本地狀態中,TemperatureInput
只能調用this.setState()
來改變它。而現在,temperature
作為prop從父組件獲取,TemperatureInput
不能再控制它了。
在React中,一般通過將組件變為“受控”,來解決這個。就像DOM<input>
接受一個value
和一個onChange
prop,所以自定義的TemperatureInput
可以從它的父組件Calculator
中獲取temperature
和onTemperatureChange
props。
現在,當TemperatureInput
想要更新它的溫度值,調用this.props.onTemperatureChange
就好了:
handleChange(e) {
// Before: this.setState({temperature: e.target.value});
this.props.onTemperatureChange(e.target.value);
注意,自定義組件中的prop名字:temperature
和onTemperatureChange
并沒什么特別的意思。我們可以隨意命名,比如給它們更通用的名字value
和onChange
。
父組件Calculator
提供proponTemperatureChange
的同時也提供temperature
。他通過修改自己的本地狀態來處理更改,從而將兩個input重新渲染為新的值。我們馬上就來看看新的Calculator
實現。
在深入Calculator
的變化之前,我們來重新看遍TemperatureInput
組件中做過什么變動。我們將他的本地狀態移除,將從this.state.temperature
讀取,改為從this.props.temperature
讀取。當我們想做出變化時,現在我們調用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>
);
}
}
現在讓我們回到Calculator
組件。
我們將當前輸入的temperature
和scale
存到他的本地狀態。這就是我們從input中“提升”的狀態,該狀態將作為“真相的來源”提供給兩個TemperatureInput
。這是我們渲染兩個input,所需數據的最少表示。
假如,我們在攝氏度輸入框中輸入37,Calculator
組件的狀態如下:
{
temperature: '37',
scale: 'c'
}
如果我們將華氏字段編輯為212,Calculator
的狀態將變為:
{
temperature: '212',
scale: 'f'
}
我們可以存儲兩個輸入的值,但實際上是不必的。存儲最后一次變化的值和單位即可。然后我們可以根據當前的溫度和單位來換算出另一個值。
因為兩個input的值從同一個狀態計算而來,所以他們始終保持同步:
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>
);
}
}
在CodePen上試一試
現在,不論你編輯哪一個input,Calculator
中的this.state.temperature
和this.state.scale
都會得到更新。從任何一個input獲取的用戶輸入都會被保存,另一個input的值會根據它重新計算。
讓我們重新看下當你編輯一個input時發生了什么:
- React調用DOM
<input>
上指定為onChange
的函數。在我們的例子中這個函數是TemperatureInput
組件的handleChange
方法。 -
TemperatureInput
中的handleChange
方法,使用新的需求值調用this.props.onTemperatureChange()
。他的props,包括onTemperatureChange
,都是由他的父組件Calculator
提供。 - 在渲染之前,
Calculator
已經將自己的handleCelsiusChange
方法賦值給攝氏度TemperatureInput
的onTemperatureChange
,還將自己的handleFaherenheitChange
方法賦值給華氏度TemperatureInput
的onTemperatureChange
。所以,Calculator
的這個兩個方法會根據我們編輯的input,而得到調用。 - 在這些方法中,
Calculator
組件通過使用新的輸入值和在編輯中input的單位來調用this.setState()
,使得React重新渲染自己。 - React通過調用
Calculator
組件的render
方法來獲取UI的外觀。兩個input的值根據當前的溫度和單位重新計算。溫度的換算在這個時候進行。 - React根據
Calculator
提供的新props來分別調用TemperatureInput
的render
方法。由此得知他們UI的外觀。 - React DOM更新DOM來匹配所需的input值。我們編輯的input接受當前的值,另一個input更新為轉換后的問題。
每次更新都會重復上面的步驟,從而使所有input保持同步。
經驗教訓
在React應用中,所有變化的數據都應該是單獨的“真相來源”。通常,狀態第一個被添加到組件中(組件需要用這些狀態來進行渲染)。如果其他組件也需要它,你可以將狀態提升到這些組件共用的最近祖先。你應該依賴自上而下的數據流,而不是同步多個組件的狀態。
比起雙向綁定的方法,提升狀態需要寫更多的“樣板”代碼,但好處就是,它可以更輕松的找到和隔離bug。因為任何狀態都是存在于組件中,并且只有組件可以修改它,由此bugs存在的區域大大被減少。另外,你可以實現任意邏輯來拒絕或轉換用戶的輸入。
如果某個值可以通過其他props或狀態獲得,那他就不該把他放在狀態中。比如,我們僅僅存儲上一次編輯的溫度和單位,而不是將celsiusValue
和fahrenheitValue
都存下來。因為在render()
方法中,一個input的值始終可以通過另一個計算得來。這樣,我們對另一個字段用或不用四舍五入,都不會在用戶的輸入中丟失精度。
當你發現UI上有錯誤發生,你可以使用React開發者工具來檢查props,然后沿著樹結構向上,知道你找到負責更新狀態的組件。這使你追溯到bug的來源: