使用 react 常常會遇到幾個組件須要共用狀態數據的狀況。這種狀況下,咱們最好將這部分共享的狀態提高至他們最近的父組件當中進行管理。咱們來看一下具體如何操做吧。javascript
這部份內容當中,咱們會建立一個溫度計算器來計算水是否會在給定的溫度下燒開。css
開始呢,咱們先建立一個名爲 BoilingVerdict
的組件。它會接受 celsius
這個溫度變量做爲它的 prop 屬性,最後根據溫度判斷返回內容:html
function BoilingVerdict(props) { if (props.celsius >= 100) { return <p>水會燒開</p>; } return <p>水不會燒開</p>; }
接下來,咱們寫一個名爲 Calculator
的組件。它會渲染一個 <input>
來接受用戶輸入,而後將輸入的溫度值保存在 this.state.temperature
當中。java
以後呢,它會根據輸入的值渲染出 BoilingVerdict
組件。react
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>輸入一個攝氏溫度</legend> <input value={temperature} onChange={this.handleChange} /> <BoilingVerdict celsius={parseFloat(temperature)} /> </fieldset> ); } }
如今咱們有了一個新的需求,在提供攝氏度輸入的基礎之上,再提供一個華氏溫度輸入,而且它們能保持同步。github
咱們能夠經過從 Calculator
組件中抽離一個 TemperatureInput
組件出來。咱們也會給它添加一個值爲 c
或 f
的表示溫度單位的 scale
屬性。函數
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 上試試。this
咱們如今有了兩個輸入框,可是當你在其中一個輸入時,另外一個並不會更新。這顯然是不符合咱們的需求的。
另外,咱們此時也不能從 Calculator
組件中展現 BoilingVerdict
的渲染結果。由於如今表示溫度的狀態數據只存在於 TemperatureInput
組件當中。
首先,咱們寫兩個能夠將攝氏度和華氏度互相轉換的函數。
function toCelsius(fahrenheit) { return (fahrenheit - 32) * 5 / 9; } function toFahrenheit(celsius) { return (celsius * 9 / 5) + 32; }
這兩個函數只是單純轉換數字。咱們還須要另一個函數,它接受兩個參數,第一個接受字符串 temperature
變量,第二個參數則是上面編寫的單位轉換函數。最後會返回一個字符串。咱們會使用它來根據一個輸入框的輸入計算出另外一個輸入框的值。
咱們最後取到輸出的小數點後三位,而 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
組件都是在本身的 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;
可是,咱們想要的是這兩個輸入能保持同步。當咱們更新攝氏輸入(Celsius)時,華氏度(Fahrenheit )這個框應該能顯示轉換後的的溫度數值,反之亦然。
在React中,狀態分享是經過將state數據提高至離須要這些數據的組件最近的父組件來完成的。這就是所謂的狀態提高。咱們會將 TemperatureInput
組件自身保存的 state 移到 Calculator
中。
若是 Calculator
組件擁有了提高上來共享的狀態數據,那它就會成爲兩個溫度輸入組件的「數據源」。它會傳遞給下面溫度輸入組件一致的數據。因爲兩個 TemperatureInput
溫度組件的props屬性都是來源於共同的父組件 Calculator
,它們的數據也會保持同步。
讓咱們一步一步來分析如何操做。
首先,咱們在 TemperatureInput
組件中將 this.state.temperature
替換爲 this.props.temperature
。從如今開始,咱們假定 this.props.temperature
屬性已經存在了,不過以後仍然須要將數據從 Calculator
組件中傳進去。
render() { // 以前的代碼: const temperature = this.state.temperature; const temperature = this.props.temperature;
咱們首先知道props是隻讀的 這麼一個事實。而以前temperature
變量是被保存在其自身的 state 中的,TemperatureInput
組件只須要調用 this.setState()
就能改變它。但如今,temperature
是做爲 prop 從父組件傳遞下來的,TemperatureInput
組件是沒有控制權的。
在React中,這個問題一般是經過讓組件「受控」來解決。就像 <input>
可以接受 value
和 onChange
這兩個prop屬性值,自定義組件 TemperatureInput
也能接受來自 Calculator
父組件的 temperature
變量和 onTemperatureChange
方法做爲props屬性值。
作完這些,當 TemperatureInput
組件更新它的溫度數值時,就會調用 this.props.onTemperatureChange
方法。
handleChange(e) { // 以前的代碼: this.setState({temperature: e.target.value}); this.props.onTemperatureChange(e.target.value);
須要指出的是,咱們如今定義的 temperature
和 onTemperatureChange
這些prop屬性的命名沒有特殊含義,咱們也能夠起個其餘任何的名字,像是value
和onChange
這些只是命名習慣罷了。
onTemperatureChange
和 temperature
兩個 props 屬性均由父組件 Calculator
提供。父組件能夠經過自身的方法來響應狀態數據的改變,從而使用新的值來從新渲染兩個輸入框組件。不過咱們先放着,最後再來修改它。
在咱們改寫 Calculator
組件以前,咱們先花點時間總結下 TemperatureInput
組件的改變。咱們將其自身的 state 從組件中移除,使用 this.props.temperature
替代 this.state.temperature
,當咱們想要響應數據改變時,使用父組件提供的 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>在{scaleNames[scale]}:中輸入溫度數值</legend> <input value={temperature} onChange={this.handleChange} /> </fieldset> ); } }
如今讓咱們來看看 Calculator
組件。
咱們將會在它的 state 中存儲以前輸入框組件的 temperature
和 scale
值,這是從輸入框組件中「提高」上來的 state,它將會成爲兩個輸入框組件的「數據源」。這是咱們所須要的可以從新渲染而且表示兩個不一樣輸入組件的最基本的數據。
舉個例子,假如咱們在攝氏度輸入框中輸入37,那麼 Calculator
的 state 就是:
{ temperature: '37', scale: 'c' }
若是咱們以後在華氏度輸入框輸入212,那麼 Calculator
的狀態數據就會是:
{ temperature: '212', scale: 'f' }
其實咱們能夠一塊兒保存兩個輸入的值,但這麼作彷佛沒有必要。保存最近 改變的值和所需標識的溫標單位就足夠了。咱們能夠只需基於當前的 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
都會更新。其中之一的輸入框獲得用戶原樣輸入的值,另外一個輸入框老是顯示基於這個值計算出的結果。
讓咱們梳理下編輯輸入框時所發生的一系列活動:
<input>
上調用指定的onChange
函數。在本例中,指的是TemperatureInput
組件上的handleChange
函數。TemperatureInput
組件的handleChange
函數會在值發生變化時調用this.props.onTemperatureChange()
函數。這些props屬性,像onTemperatureChange
都是由父組件Calculator
提供的。Calculator
組件把內部的handleCelsiusChange
方法指定給攝氏輸入組件TemperatureInput
的onTemperatureChange
方法,而且把handleFahrenheitChange
方法指定給華氏輸入組件TemperatureInput
的onTemperatureChange
。兩個Calculator
內部的方法都會在相應輸入框被編輯時被調用。Calculator
組件會讓React使用編輯輸入的新值和當前輸入框的溫標來調用this.setState()
方法來重渲染自身。Calculator
組件的render
方法來識別UI界面的樣子。基於當前溫度和溫標,兩個輸入框的值會被從新計算。溫度轉換就是在這裏被執行的。Calculator
指定的新props來分別調用TemperatureInput
組件.React也會識別出子組件的UI界面。一切更新都是通過一樣的步驟,於是輸入框能保持同步的。
在React應用中,對應任何可變數據理應只有一個單一「數據源」。一般,狀態都是首先添加在須要渲染數據的組件中。此時,若是另外一個組件也須要這些數據,你能夠將數據提高至離它們最近的父組件中。你應該在應用中保持 自上而下的數據流,而不是嘗試在不一樣組件中同步狀態。
狀態提高比雙向綁定方式要寫更多的「模版代碼」,但帶來的好處是,你也能夠更快地尋找和定位bug的工做。由於哪一個組件保有狀態數據,也只有它本身可以操做這些數據,發生bug的範圍就被大大地減少了。此外,你也可使用自定義邏輯來拒絕或者更改用戶的輸入。
若是某些數據能夠由props或者state提供,那麼它頗有可能不該該在state中出現。舉個例子,咱們僅僅保存最新的編輯過的temperature
和scale
值,而不是同時保存 celsiusValue
和 fahrenheitValue
。另外一個輸入框中的值老是能夠在 render()
函數中由這些保存的數據計算出來。這樣咱們能夠根據同一個用戶輸入精準計算出兩個須要使用的數據。
當你在開發UI界面遇到問題時,你可使用 React 開發者工具來檢查props屬性,而且能夠點擊查看組件樹,直到你找到負責目前狀態更新的組件。這能讓你到追蹤到產生 bug 的源頭。