# 提升狀態
通常,一些組件需要反映相同的修改數據。我們建議提升共享的狀態到它們最近的共同祖先組件。我們看下這是如何運作的。
在本節,我們將創建一個溫度計算器,計算水在一個給定溫度是否會沸騰。
我們通過一個稱為 BoilingVerdict 的組件開始。它接受攝氏溫度作為 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.value 中保存它的值。
另外,它對當前的輸入值渲染 BoiliingVerdict 。
~~~
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>
);
}
}
~~~
在 CodePen 中[打開查看](http://codepen.io/gaearon/pen/Gjxgrj?editors=0010)。
## 添加第二個 input
我們新的需求是,除了輸入一個攝氏溫度,我們還提供了一個華氏溫度輸入,它們保持同步。
我們可以從 Calculator 中提取一個 TemperatureInput 組件開始。我們將添加一個新的 scale 屬性,值可能是 “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>
);
}
}
~~~
現在我們可以修改 Calculator 來渲染兩個單獨的溫度輸入:
~~~
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
~~~
在 CodePen 中[打開查看](http://codepen.io/gaearon/pen/NRrzOL?editors=0010)。
我們現在有兩個 input 了,但是當你輸入了其中一個溫度,其它的并沒有更新。這是跟我們的需要不符的:我們希望保持它們同步。
我們也不能從 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' 。
接下來,我們移除 TemperatureInput 中的 state 。
相反,它通過 props 接受 value 和 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 (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={value}
onChange={this.handleChange} />
</fieldset>
);
}
}
~~~
如果一些組件需要訪問相同的狀態,這是一個信號說明 state 應該被提升狀態到它們最近的祖先了。在這里,這個組件是 Calculator。我們將保存當前的 value 和 scale 到它的 state。
我們將保存 input 的 value ,但是它結果是不必要的。保存 value 和它表示的 scale 到最新修改的 input 就足夠了。然后我們可以根據當前的 value 和 scale 推斷出另一個 input 的 value。
input 會保持同步因為它們的值從同一個 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 (
<div>
<TemperatureInput
scale="c"
value={celsius}
onChange={this.handleCelsiusChange} />
<TemperatureInput
scale="f"
value={fahrenheit}
onChange={this.handleFahrenheitChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</div>
);
}
}
~~~
在 CodePen 中[打開查看](http://codepen.io/gaearon/pen/ozdyNg?editors=0010)。
現在,無論你編輯哪個 input ,Calculator 中的 this.state.value 和 this.state.scale 都會得到更新。一個輸入框獲得值,那么任何用戶輸入都會被保存,另一個輸入框的值也總是基于它進行重新計算。
## 經驗總結
在 一個 React 應用中,對于任何修改的數據應該有一個單獨的“真實源”。通常,state 首先被添加到需要它進行渲染的組件。然后,如果其它的組件也需要它,你可以提升狀態到它們最近的祖先。你應該依賴[從上到下的數據流向](https://facebook.github.io/react/docs/state-and-lifecycle.html#the-data-flows-down),而不是試圖在不同的組件中同步狀態。
提升狀態涉及到比編寫兩種方式的綁定策略更“樣板化”的代碼,但是有一個好處,它可以更方便的找出并遠離 bugs。由于任何狀態都生存在一些組件中,而且組件單獨的修改它,發生錯誤的可能大大減少。另外,你可以實現任何定制的邏輯來拒絕或者轉換用戶輸入。
如果可以從 props 或者 state 得來,它可能不是在 state 中。例如,我們只保存最后編輯的 value 和它的 sacle,而不是保存 celsiusValue 和 fahrenheitValue 。另外的輸入框總是在 render() 方法中計算得來。這使我們清除或者應用舍入其它字段而不會丟失任何用戶輸入的精度。
當你看到 UI 中的錯誤,你可以使用 [React 開發者工具](https://github.com/facebook/react-devtools)來檢查 props 并遍歷樹,直到找出負責更新狀態的組件。這使你可以跟蹤到 bug 的源頭:
