天天看點

狀态提升(精讀React官方文檔—10)

這是我參與更文挑戰的第23天,活動詳情檢視: 更文挑戰

為什麼需要狀态提升?

有時候,多個元件需要共享狀态,此時需要将共享狀态提升到最近的共同父元件中去。

首先建立一個判斷水是否沸騰的元件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 的元件。它渲染一個用于輸入溫度的 <input>,并将其值儲存在 this.state.temperature 中。另外, 它根據目前輸入值渲染 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>
    );
  }
}
複制代碼      

添加第二個輸入框

在已有攝氏溫度輸入框的基礎上,我們提供華氏度的輸入框,并保持兩個輸入框的資料同步。 先從 Calculator 元件中抽離出 TemperatureInput 元件,然後為其添加一個新的 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 元件讓它渲染兩個獨立的溫度輸入框元件:
class Calculator extends React.Component {
  render() {
    return (
      <div>
        <TemperatureInput scale="c" />
        <TemperatureInput scale="f" />
      </div>
    );
  }
}
複制代碼      

解讀

我們希望的是當一個輸入框的值發生變化的時候,另一個輸入框的值也發生變化。即讓他們保持同步。

編寫轉換函數

編寫兩個可以在攝氏度與華氏度之間互相轉換的函數:
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;
    // ...  
複制代碼      

我們要想保持兩個輸入框中内容的同步,必須将子元件的狀态移動到其共同的父元件中。

首先我們将 TemperatureInput 元件中的 this.state.temperature 替換為 this.props.temperature。
render() {
    // Before: const temperature = this.state.temperature;
    const temperature = this.props.temperature;
    // ...
複制代碼      

由于props是隻讀的,由于temperature是通過props傳遞過來的,是以子元件現在失去了對它的控制權。是以此時我們要想繼續修改temperature我們需調用this.props.onTemperatureChange 來更新它:

handleChange(e) {
    // Before: this.setState({temperature: e.target.value});
    this.props.onTemperatureChange(e.target.value);
    // ...
複制代碼      
官方提示:自定義元件中的 temperature 和 onTemperatureChange 這兩個 prop 的命名沒有任何特殊含義。我們可以給它們取其它任意的名字,例如,把它們命名為 value 和 onChange 就是一種習慣。

階段性回顧

我們首先移出了元件自身的state,過使用 this.props.temperature 替代 this.state.temperature 來讀取溫度資料。當我們想要響應資料改變時,我們需要調用 Calculator 元件提供的 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>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}
複制代碼      
我們可以存儲兩個輸入框中的值,但這并不是必要的。我們隻需要存儲最近修改的溫度及其計量機關即可,根據目前的 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>
    );
  }
}
複制代碼      

梳理回顧整個流程

  1. React會調用每一個TemperatureInput中的handleChange方法
  2. 調用的handleChange方法均有父元件提供
  3. 無論哪個輸入框的輸入發生變化都會調用父元件更新兩個輸入框中的狀态值

為什麼任何可變資料應當隻有一個相對應的唯一資料源?

帶來的好處是,排查和隔離 bug 所需的工作量将會變少。由于“存在”于元件中的任何 state,僅有元件自己能夠修改它,是以 bug 的排查範圍被大大縮減了。

歡迎大家關注我的專欄,一起學習React!

繼續閱讀