React 提升共用狀態 (Lifting State Up) | 子元件事件 (Child Events)
很常你會遇到一種情況是,有好幾個獨立元件同時都跟某個資料狀態 (shared state) 的改變有相依性,React 建議的處理模式是,將這個資料狀態提升 (lifting state up) 到這些元件的某個共同父元件 (closest common ancestor) 上面。
實作概念是,當子元件事件 (Child Events) 發生時,透過父元件傳進來的 callback function 通知父元件有資料變動,然後由父元件統一計算得到新的父元件 state,然後透過 props 反應資料更新回這些子元件。
這 design pattern 幫助程式更乾淨好維護,不會在很多元件中都有一樣的 state copy 需要維護資料同步的問題,也幫助你更好 debug,因為只有一個 source of truth 就是父元件的 state。
拿一個 "判斷水溫是否已經沸騰" 的程式當例子,我們有兩個輸入欄位,分別用來輸入攝氏溫度或華氏溫度,讓使用者可以在任一欄位輸入溫度來判斷溫度是否已達沸騰,這兩個欄位的溫度需要做自動單位換算隨時保持兩邊的溫度值同步:
const scaleNames = {
c: '攝氏溫度',
f: '華氏溫度'
};
// 從華氏溫度轉換成攝氏溫度
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) {
// 當超過攝氏 100 度告訴使用者水已經沸騰
if (props.celsius >= 100) {
return <p>水已經沸騰囉!</p>;
}
return <p>水還沒沸騰,還要再煮一會兒</p>;
}
// 溫度輸入元件
// 這是一個子元件
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
// 當 input value 改變時,透過 parent 傳進來的 callback 通知上層
this.props.onTemperatureChange(e.target.value);
}
render() {
const temperature = this.props.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>請輸入溫度 - {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);
// 統一在父元件這邊維護一個共用的 state
this.state = {temperature: '', scale: 'c'};
}
// 傳進子元件的 callback function
// 當子元件中攝氏溫度欄位改變時 call 這個 function 通知父元件
handleCelsiusChange(temperature) {
this.setState({scale: 'c', temperature});
}
// 傳進子元件的 callback function
// 當子元件中華氏溫度欄位改變時 call 這個 function 通知父元件
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;
// 將最新的溫度 state,透過 props 同步到所有需要這資料的子元件中
// 將 callback function 也透過 props 傳進子元件,用來讓子元件 Lifting State Up
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')
);