[譯]React核心概念9:狀態提高

原文連接:reactjs.org/docs/liftin…html

引言

不少狀況下咱們使用的多個組件須要對同一個數據作出對應的反應。在這裏咱們推薦把這個共享的狀態提高到距離這些組件最近的祖先組件。如今讓咱們來看看這是怎麼工做的。react

在本章中,咱們將會建立一個溫度計算器來計算在給定溫度下水是否會沸騰。git

首先咱們現建立一個BoilingVerdict組件。它接收一個celsius(攝氏度)做爲prop,並在頁面上打印出在這個溫度下水是否會沸騰。github

function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
}
複製代碼

接下來,咱們將會建立一個Calculator組件。它渲染了一個輸入框來輸入溫度並將輸入值綁定到this.state.temperaturebash

除此以外,它也爲當前輸入的溫度渲染相應的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>
    );
  }
}
複製代碼

添加第二個輸入框

如今咱們有一個新需求,除了輸入攝氏溫度以外,咱們還須要一個可以輸入華氏溫度的輸入框,而且這兩個輸入框是同步的。ui

首先咱們能夠從Calculator組件中提取出一個TemperatureInput組件。並添加一個scale做爲prop來表明攝氏度(c)或華氏溫度(f)。this

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組件來渲染兩個不一樣的溫度輸入。spa

class Calculator extends React.Component {
  render() {
    return (
      <div>
        <TemperatureInput scale="c" />
        <TemperatureInput scale="f" />
      </div>
    );
  }
}
複製代碼

如今咱們有了兩個輸入框,可是當你在其中一個輸入時另外一個輸入框內的數據並不會更新。這就與咱們想要這兩個輸入框同步的需求矛盾了。雙向綁定

咱們也無法在Calculator中展現BoilingVerdict了,由於Calculator組件沒有辦法獲取到隱藏在TemperatureInput組件裏的溫度。

編寫轉換函數

爲了解決上述的矛盾,咱們現編寫可以相互轉換攝氏度和華氏溫度的兩個函數。

function toCelsius(fahrenheit) {
  return (fahrenheit - 32) * 5 / 9;
}

function toFahrenheit(celsius) {
  return (celsius * 9 / 5) + 32;
}
複製代碼

這兩個函數用來轉換數字,如今咱們來編寫另外一個函數,這個函數講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;
    // ...  
複製代碼

可是咱們想要這兩個輸入框可以同步數據,當咱們在攝氏度輸入時,華氏溫度可以同時顯示通過轉化後的溫度,反之亦然。

在React中,共享狀態的方法是將state轉移到離須要共享組件最近的公共父組件中,這稱爲「狀態提高」。如今咱們將把TemperatureInput組件中的state轉移到Calculator中。

若是Calculator包含了共享狀態,那麼它就是這兩個溫度輸入組件的「數據源」,這能使兩個輸入組件的數據始終保持一致。 由於兩個TemperatureInput組件的props都是來自共同的父組件Calculator的,因此他們在數據顯示上可以保持同步。

如今咱們來逐步瞭解這是怎麼完成的。

首先,咱們將TemperatureInput組件中的this.state.temperture替換成this.props.temperature。固然this.props.temperature是經過Calculator組件傳遞的。

render() {
    // Before: const temperature = this.state.temperature;
    const temperature = this.props.temperature;
    // ...
複製代碼

咱們知道props是隻讀的,當咱們將溫度存儲在本地state時,咱們能夠經過setState()來修改它。可是如今溫度是經過父組件傳遞進來的,那麼TemperatureInput組件就沒有修改溫度的權限了。

React中,一般的解決方案是將組件變爲「受控」的。就像在DOM中<input>接收valueonChange做爲prop同樣,咱們可讓TemperatureInput組件接收從Calculator傳遞來的prop:temperatureonTemperatureChange

如今,當TemperatureInput想要更新溫度時,只要調用this.props.onTemperatureChange就好了:

handleChange(e) {
    // Before: this.setState({temperature: e.target.value});
    this.props.onTemperatureChange(e.target.value);
    // ...
複製代碼

提示 temperature和onTemperatureChange在這裏沒有特殊的含義,只是一個屬性名稱,是能夠隨意定義的,它也能夠是更大衆化的value和onChange。

onTermperatureChange屬性和temperature屬性都是由父組件Calculator提供的。它會經過修改自身的state來處理數據的變化,以此來從新渲染兩個輸入框內的數據。咱們很快就能夠看到Calculator組件的實現細節。

在咱們深刻了解Calculaor組件的實現以前,先讓咱們來回顧一下TemperatureInput組件作了哪些修改。咱們將移除了本地state,將本來經過讀取this.state.temperature獲取溫度替換成讀取this.props.temperature獲取,將經過調用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組件。

咱們將輸入框的temperature和scale存儲在本地state中,這是咱們從input中提高出來的狀態,來做爲兩個input的數據源。這就是咱們渲染兩個input須要的最小數據集合。

舉個例子,如今咱們在攝氏度輸入框內輸入37,那麼Calculator組件的state就將會是這樣:

{
  temperature: '37',
  scale: 'c'
}
複製代碼

若是以後咱們在華氏溫度輸入框中輸入212,Calculator的state就將變成:

{
  temperature: '212',
  scale: 'f'
}
複製代碼

有人會說爲何存儲兩個input的值?能夠,但不必。只要存儲最近修改的值和它所表明的scale就足夠了。由於咱們能夠根據當前的temperature和scale推算出另外一個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>
    );
  }
}
複製代碼

如今,不管你在哪一個輸入框輸入,Calculator組件的this.state.scalethis.state.temperature都會更新。其中一個輸入框的輸入按照原樣取值,因而用戶的輸入就這樣被保存了,而另外一個輸入框的值就會根據用戶的輸入推算出來。

讓咱們梳理如下當咱們在一個輸入框輸入時代碼的運行邏輯:

  • React調用DOM標籤<input>上的onChange方法,在咱們的例子中,調用的時TemperatureInput組件的handleChange方法。
  • TemperatureInput組件中的handleChange被調用時,它將最新的數據傳遞給this.props.onTemperatureChange()。這裏的props都由父組件Calculator提供。
  • 以前渲染的時候,Calculator組件在攝氏度的TemperatureInput組件上聲明的onTemperatureChange事件所調用的方法是handleCelsiusChange,在華氏溫度上的則是handleFahrenheitChange。因此這兩個函數的調用取決於咱們在哪一個輸入框輸入數據。
  • 在這些方法中,Calculator組件調用setState()將最新輸入的值和scale從新渲染。
  • React調用Calculator組件的render()方法將UI渲染在頁面上。兩個輸入框的值都在此時根據當前的狀態從新計算,溫度的換算也在此時進行。
  • React調用兩個TemperatureInput組件的render()方法,並根據由Calculator定義的props渲染在頁面上。
  • React調用BoilingVerdict組件的render()方法,並根據傳進來的攝氏度溫度渲染數據。
  • React DOM更新沸騰結果和輸入框的內容。咱們剛剛編輯的輸入框接收當前的值,而另外一個輸入框接收通過轉換的值。

每一次更新都按照上述步驟進行,因此兩個輸入框數據能夠保持同步。

經驗總結

在React應用中任何可變的數據都應該有惟一數據源。一般來講,state一開始都是包含在須要根據它來渲染數據的組件中。可是若是有其餘組件也想要使用這個state,那麼就須要把它提高到距離這兩個組件最近的公共父組件中。可是相比於保持不一樣組件的數據同步,咱們更應該依靠的是自頂而下的數據流。

相較於雙向綁定,狀態提高須要編寫更多的「樣板」代碼。但這樣作的好處是咱們能夠更好地定位和分離bug。由於state只存在於各自的組件中,因此bug出現範圍就大大減小了。除此以外,咱們能夠自由地實現任何邏輯來拒絕或修改用戶輸入。

若是某個數據能夠根據state或者props推導出來,那麼這個數據就不該該存儲在state中。就像本章的實例代碼中,咱們只存儲最新輸入的溫度和它對應的scale,並不須要將華氏溫度和攝氏溫度都存儲在state中。另外一個輸入框的數據在調用render()方法時就能夠根據當前的數據推算出來。這能讓咱們清除用戶輸入或者在不損失用戶輸入精度的狀況下在其餘區域使用用戶輸入的值。

當頁面出現錯誤時,你可使用React Developer Tools查看props而且在組件樹上追溯源頭直到找到對應的組件爲止。這可以讓你輕鬆地定位bug。

相關文章
相關標籤/搜索