提升 State
通常來說,有一些 component 需要反映相同的資料變化。我們建議將共享的 state 提升到最靠近它們的共同 ancestor。讓我們來看這是如何運作的。
在這個章節,我們將建立一個溫度計算器來計算水是否會在給定的溫度下沸騰。
我們將會建立一個 component 叫做 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
的 component。它 render 一個 <input>
讓你輸入溫度,並且將它的值儲存在 this.state.temperature
。
此外,它 render 目前輸入值在 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>
);
}
}
加入第二個輸入
這是我們的新需求,除了攝氏輸入,我們提供一個華氏輸入,它們彼此保持同步。
我們會從 Calculartor
抽離出一個 TemperatureInput
component。我們將會加入一個新的 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
來 render 兩個獨立的溫度輸入:
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" /> <TemperatureInput scale="f" /> </div>
);
}
}
現在我們有兩個輸入,但是當你輸入其中一個溫度輸入時,另外一個輸入並沒有更新。這和我們的需求產生了矛盾:我們希望它們可以保持同步。
我們也無法從 Calculator
顯示 BoilingVerdict
。Calculator
並不知道目前的溫度,因為它被隱藏在 TemperatureInput
內。
撰寫轉換 Function
首先,我們將撰寫兩個 function 來轉換攝氏和華氏:
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
這兩個 function 轉換 number。我們將撰寫其他 function,它將 temperature
string 和轉換 function 作為參數,並回傳 string。我們將使用它來計算基於另一個輸入的輸入值。
當 temperatrue
無效時,回傳一個空的 string,它的輸出會四捨五入到小數點後第三位:
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)
回傳一個空的 string,而 tryConvert('10.22', toFahrenheit)
回傳 '50.396'
。
Lifting State Up
目前兩個 TemperatureInput
component 都有它們各自的 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 搬移到需要它的 component 共同最近的 ancestor 來完成的。這被稱為「提升 state」。我們將從 TemperatureInput
移除 local state 並且搬移它到 Calculator
。
如果 Calculator
擁有共享 state,它將成為目前兩個溫度輸入的「真相來源」。這可以說明它們兩者具有一致的值。由於這兩個 TemperatureInput
component 的 prop 都是來自相同的 Calculator
parent component,所以這兩個輸入會彼此同步。
讓我們來一步一步看它是如何執行的。
首先,我們將會把 TemperatureInput
component 的 this.state.temperature
替換為 this.props.temperature
。現在,讓我們假設 this.props.temperature
已經存在,雖然我們之後需要從 Calculator
傳遞它:
render() {
// 先前:const temperature = this.state.temperature;
const temperature = this.props.temperature; // ...
我們知道 prop 是唯讀的。當 temperature
在 local state 時,TemperatureInput
可以呼叫 this.setState()
來改變它。然而,現在 temperature
prop 是來自它的 parent,TemperatureInput
無法控制它。
在 React 中,這通常透過讓 component「被控制」來解決。就像 DOM <input>
同時接受 value
和 onChange
prop,所以可以自訂 TemperatureInput
同時接受來自 Calculator
parent component 的 temperature
和 onTemperatureChange
prop。
現在,當 TemperatureInput
想要更新溫度時,它呼叫 this.props.onTemperatureChange
:
handleChange(e) {
// 先前:this.setState({temperature: e.target.value});
this.props.onTemperatureChange(e.target.value); // ...
注意:
Component 中自訂的
temperature
或onTemperatureChange
prop 名稱並沒有特殊含義。我們可以將它們命名為任何名稱,像是value
和onChange
,這是常見的慣例。
onTemperatureChange
prop 與 temperature
prop 將會由 Calculator
parent component 提供。它將透過修改本身的 local state 來處理更改,因此會重新 render 兩個輸入與新的值。我們將快速地看一下 Calculator
的實作。
在深入改變 Calculator
之前,讓我們回顧先前對 TemperatureInput
的修改。我們移除了它的 local state,並且不讀取 this.state.temperature
,我們現在讀取 this.props.temperature
。當我們想要改變時不呼叫 this.setState()
,我們現在呼叫 this.props.onTemperatureChange()
,它是由 Calculator
提供的:
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
component。
我們將 store 目前輸入的 temperature
和 scale
在它的 local state。這是我們從輸入「提升」的 state,而且同時為輸入的「真相來源」。它是我們為了 render 兩個輸入而需要知道的最小表示資料。
例如,如果我們輸入 37 到攝氏輸入,Calculator
component 的 state 將會是:
{
temperature: '37',
scale: 'c'
}
如果我們之後更改華氏欄位為 212,Calculator
component 的 state 將會是:
{
temperature: '212',
scale: 'f'
}
我們可以 store 兩個輸入的值,但事實證明它是不需要的。它只要 store 最近修改的輸入值,以及溫度單位就夠了我們可以根據目前的 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>
);
}
}
現在,無論輸入如何改變,在 Calculator
的 this.state.temperature
和 this.state.scale
都會被更新。其中一個輸入保留任何使用者的輸入,並且總是根據使用者的輸入重新計算另一個輸入值。
讓我們回顧一下當你編輯輸入時會發生什麼事情:
- React 在 DOM
<input>
上呼叫被指定為onChange
的函式。在我們的範例中,這是在TemperatureInput
component 內的handleChange
方法。 - 在
TemperatureInput
component 的handleChange
方法呼叫this.props.onTemperatureChange()
與新的期望值。它的 prop 包含onTemperatureChange
,是由Calculator
parent component 所提供的。 - 當它被 render 之前,
Calculator
指定攝氏TemperatureInput
的onTemperatureChange
是Calculator
的handleCelsiusChange
方法,而華氏溫度的TemperatureInput
的onTemperatureChange
方法是Calculator
的handleFahrenheitChange
方法。因此根據我們編輯的輸入呼叫這兩個Calculator
方法中的其中一個。 - 在這些方法中,
Calculator
component 要求 React 根據我們編輯的新輸入值和目前的溫度單位的輸入呼叫this.setState()
來重新 render 本身。 - React 呼叫
Calculator
component 的render
方法來了解 UI 應該是怎麼樣子。根據目前溫度和溫度單位重新計算兩個輸入的值。溫度轉換會在這裡執行。 - 透過
Calculator
指定新的 prop,React 呼叫各個TemperatureInput
component 的render
方法,它們應該了解 UI 是什麼樣子。 - React 呼叫
BoilingVerdict
component 的render
方法,以攝氏溫度做為 prop。 - React DOM 使用沸騰判定更新 DOM 並匹配所需的輸入值。我們剛剛編輯的輸入它接收目前的值,而另一個輸入被更新成轉換後的溫度。
每次更新都會執行相同的步驟,保持輸入的同步。
經驗學習
在 React 應用程式中,對於資料的變化只能有一個唯一的「真相來源」。通常來說,state 會優先被加入到需要 render 的 component。接著,如果其他的 component 也需要的話,你可以提升 state 到共同最靠近的 ancestor。你應該依賴上至下的資料流,而不是嘗試在不同 component 之間同步 state。
提升 state 涉及撰寫更多的「boilerplate」程式碼,而不是雙向綁定的方法,但它對於隔離和尋找 bug 時更加容易。由於任何 state「存活」在一些 component 中,而且 component 本身可以改變它,bug 的產生大幅的減少。此外,你也可以實作任何自訂的邏輯來拒絕或轉換使用者的輸入。
如果某樣東西可以從 prop 或 state 被取得,它可能不應該在 state。例如,我們只 store 最後編輯的 temperature
和它的 scale
,而不是 store celsiusValue
和 fahrenheitValue
。其他輸入的值總是可以從它們的 render()
方法被計算出來。這讓我們可以清除或將四捨五入應用於另一個欄位, 而不會在使用者輸入中失去任何精度。
當你在 UI 上看到一些錯誤時,你可以使用 React Developer Tools 來檢查 prop 並往 tree 的上方尋找,直到找到負責更新 state 的 component。這讓你可以追蹤到錯誤的來源: