提升狀態(tài)

通常,幾個(gè)組件需要根據(jù)同一個(gè)數(shù)據(jù)變化做出響應(yīng)。我們建議將這個(gè)共享的狀態(tài)提升到他們最近的一個(gè)共用祖先。讓我們看看實(shí)際該怎么做。
在這一節(jié),我們將創(chuàng)建一個(gè)溫度計(jì)算器,用來(lái)計(jì)算一給定溫度能否讓水沸騰。
我們從名為BoilingVerdict的組件開(kāi)始。它接受celsius溫度作為prop,然后打印出是否足夠使水沸騰:

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

接下來(lái),我們創(chuàng)建一個(gè)Calculator組件。它渲染一個(gè)供你輸入溫度的<input>,并將它的值存在this.state.temperature中。
除此之外,他還為當(dāng)前的輸入渲染一個(gè)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>
    );
  }
}

在CodePen上試一試

添加第二個(gè)輸入

我們有個(gè)新的需求,除了一個(gè)攝氏度輸入,我們還要提供一個(gè)華氏輸入,并且他們保持同步。
我們先從Calculator中提取TemperatureInput組件。然后為其添加一個(gè)新的scaleprop,它的值值為"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>
    );
  }
}

現(xiàn)在我們可以改變Calculator來(lái)渲染兩個(gè)獨(dú)立的溫度輸入:

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

在CodePen上試一試
現(xiàn)在我們有兩個(gè)輸入了,但是當(dāng)你在其中一個(gè)里輸入溫度,另一個(gè)不會(huì)去更新。這就不滿足我們的需求了,我們想讓他們同步。
我們也沒(méi)在Calculator中顯示BoilingVerdictCalculator不知道當(dāng)前的溫度,因?yàn)闇囟缺浑[藏在TemperatureInput中。

編寫(xiě)轉(zhuǎn)換函數(shù)

首先我們寫(xiě)兩個(gè)函數(shù)來(lái)互相轉(zhuǎn)換攝氏度和華氏溫度。

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

function toFahrenheit(celsius) {
  return (celsius * 9 / 5) + 32;
}

這兩個(gè)函數(shù)轉(zhuǎn)換數(shù)字。接下來(lái)我們寫(xiě)另一個(gè)函數(shù),接受一個(gè)temperature字符串和一個(gè)轉(zhuǎn)換函數(shù)作為參數(shù),返回一個(gè)字符串。我們將用他來(lái)根據(jù)另一個(gè)input來(lái)計(jì)算這個(gè)input的值。
一個(gè)無(wú)效temperature會(huì)使它返回一個(gè)空字符串,另外它會(huì)保證輸出結(jié)果四舍五入到小數(shù)點(diǎn)后三位:

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'。

提升狀態(tài)

目前,兩個(gè)TemperatureInput組件都單獨(dú)地在本地狀態(tài)中保存自己的值:

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;

然而,我們希望這兩個(gè)input可以彼此同步。當(dāng)我們更新攝氏度input,華氏度input也會(huì)相應(yīng)的轉(zhuǎn)換溫度,反之亦然。
在React中,想要共享狀態(tài),就需要找到共享狀態(tài)組件的一個(gè)最近共有祖先,然后通過(guò)將該狀態(tài)移動(dòng)到這個(gè)共有祖先上來(lái)完成共享。這稱(chēng)為“提升狀態(tài)”。我們先移除TemperatureInput的本地狀態(tài),取而代之的是將它移到Calculator中。
如果Calculator擁有共享狀態(tài),對(duì)于兩個(gè)input中的溫度來(lái)說(shuō)他就成為了“真相的來(lái)源”。他就可以指示兩個(gè)input具有相同的值。由于兩個(gè)TemperatureInput組件的props都來(lái)自同一個(gè)父組件Calculator,兩個(gè)input將始終保持同步。
讓我們逐步分析這是如何工作的。
首先,在TemperatureInput組件中,我們使用this.props.temperaturethis.state.temperature替換掉?,F(xiàn)在,讓我們假設(shè)this.props.temperature已經(jīng)存在,之后我們會(huì)從Calculator中傳入該值:

  render() {
    // Before: const temperature = this.state.temperature;
    const temperature = this.props.temperature;

我們知道props是只讀的。之前temperature在本地狀態(tài)中,TemperatureInput只能調(diào)用this.setState()來(lái)改變它。而現(xiàn)在,temperature作為prop從父組件獲取,TemperatureInput不能再控制它了。
在React中,一般通過(guò)將組件變?yōu)椤笆芸亍?,?lái)解決這個(gè)。就像DOM<input>接受一個(gè)value和一個(gè)onChangeprop,所以自定義的TemperatureInput可以從它的父組件Calculator中獲取temperatureonTemperatureChangeprops。
現(xiàn)在,當(dāng)TemperatureInput想要更新它的溫度值,調(diào)用this.props.onTemperatureChange就好了:

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

注意,自定義組件中的prop名字:temperatureonTemperatureChange并沒(méi)什么特別的意思。我們可以隨意命名,比如給它們更通用的名字valueonChange。
父組件Calculator提供proponTemperatureChange的同時(shí)也提供temperature。他通過(guò)修改自己的本地狀態(tài)來(lái)處理更改,從而將兩個(gè)input重新渲染為新的值。我們馬上就來(lái)看看新的Calculator實(shí)現(xiàn)。
在深入Calculator的變化之前,我們來(lái)重新看遍TemperatureInput組件中做過(guò)什么變動(dòng)。我們將他的本地狀態(tài)移除,將從this.state.temperature讀取,改為從this.props.temperature讀取。當(dāng)我們想做出變化時(shí),現(xiàn)在我們調(diào)用Calculator提供的this.props.onTemperatureChange(),來(lái)代替之前的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>
    );
  }
}

現(xiàn)在讓我們回到Calculator組件。
我們將當(dāng)前輸入的temperaturescale存到他的本地狀態(tài)。這就是我們從input中“提升”的狀態(tài),該狀態(tài)將作為“真相的來(lái)源”提供給兩個(gè)TemperatureInput。這是我們渲染兩個(gè)input,所需數(shù)據(jù)的最少表示。
假如,我們?cè)跀z氏度輸入框中輸入37,Calculator組件的狀態(tài)如下:

{
  temperature: '37',
  scale: 'c'
}

如果我們將華氏字段編輯為212,Calculator的狀態(tài)將變?yōu)椋?/p>

{
  temperature: '212', 
  scale: 'f'
}

我們可以存儲(chǔ)兩個(gè)輸入的值,但實(shí)際上是不必的。存儲(chǔ)最后一次變化的值和單位即可。然后我們可以根據(jù)當(dāng)前的溫度和單位來(lái)?yè)Q算出另一個(gè)值。
因?yàn)閮蓚€(gè)input的值從同一個(gè)狀態(tài)計(jì)算而來(lái),所以他們始終保持同步:

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>
    );
  }
}

在CodePen上試一試
現(xiàn)在,不論你編輯哪一個(gè)input,Calculator中的this.state.temperaturethis.state.scale都會(huì)得到更新。從任何一個(gè)input獲取的用戶(hù)輸入都會(huì)被保存,另一個(gè)input的值會(huì)根據(jù)它重新計(jì)算。
讓我們重新看下當(dāng)你編輯一個(gè)input時(shí)發(fā)生了什么:

  • React調(diào)用DOM<input>上指定為onChange的函數(shù)。在我們的例子中這個(gè)函數(shù)是TemperatureInput組件的handleChange方法。
  • TemperatureInput中的handleChange方法,使用新的需求值調(diào)用this.props.onTemperatureChange()。他的props,包括onTemperatureChange,都是由他的父組件Calculator提供。
  • 在渲染之前,Calculator已經(jīng)將自己的handleCelsiusChange方法賦值給攝氏度TemperatureInputonTemperatureChange,還將自己的handleFaherenheitChange方法賦值給華氏度TemperatureInputonTemperatureChange。所以,Calculator的這個(gè)兩個(gè)方法會(huì)根據(jù)我們編輯的input,而得到調(diào)用。
  • 在這些方法中,Calculator組件通過(guò)使用新的輸入值和在編輯中input的單位來(lái)調(diào)用this.setState(),使得React重新渲染自己。
  • React通過(guò)調(diào)用Calculator組件的render方法來(lái)獲取UI的外觀。兩個(gè)input的值根據(jù)當(dāng)前的溫度和單位重新計(jì)算。溫度的換算在這個(gè)時(shí)候進(jìn)行。
  • React根據(jù)Calculator提供的新props來(lái)分別調(diào)用TemperatureInputrender方法。由此得知他們UI的外觀。
  • React DOM更新DOM來(lái)匹配所需的input值。我們編輯的input接受當(dāng)前的值,另一個(gè)input更新為轉(zhuǎn)換后的問(wèn)題。

每次更新都會(huì)重復(fù)上面的步驟,從而使所有input保持同步。

經(jīng)驗(yàn)教訓(xùn)

在React應(yīng)用中,所有變化的數(shù)據(jù)都應(yīng)該是單獨(dú)的“真相來(lái)源”。通常,狀態(tài)第一個(gè)被添加到組件中(組件需要用這些狀態(tài)來(lái)進(jìn)行渲染)。如果其他組件也需要它,你可以將狀態(tài)提升到這些組件共用的最近祖先。你應(yīng)該依賴(lài)自上而下的數(shù)據(jù)流,而不是同步多個(gè)組件的狀態(tài)。
比起雙向綁定的方法,提升狀態(tài)需要寫(xiě)更多的“樣板”代碼,但好處就是,它可以更輕松的找到和隔離bug。因?yàn)槿魏螤顟B(tài)都是存在于組件中,并且只有組件可以修改它,由此bugs存在的區(qū)域大大被減少。另外,你可以實(shí)現(xiàn)任意邏輯來(lái)拒絕或轉(zhuǎn)換用戶(hù)的輸入。
如果某個(gè)值可以通過(guò)其他props或狀態(tài)獲得,那他就不該把他放在狀態(tài)中。比如,我們僅僅存儲(chǔ)上一次編輯的溫度和單位,而不是將celsiusValuefahrenheitValue都存下來(lái)。因?yàn)樵?code>render()方法中,一個(gè)input的值始終可以通過(guò)另一個(gè)計(jì)算得來(lái)。這樣,我們對(duì)另一個(gè)字段用或不用四舍五入,都不會(huì)在用戶(hù)的輸入中丟失精度。
當(dāng)你發(fā)現(xiàn)UI上有錯(cuò)誤發(fā)生,你可以使用React開(kāi)發(fā)者工具來(lái)檢查props,然后沿著樹(shù)結(jié)構(gòu)向上,知道你找到負(fù)責(zé)更新?tīng)顟B(tài)的組件。這使你追溯到bug的來(lái)源:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 最近看了一本關(guān)于學(xué)習(xí)方法論的書(shū),強(qiáng)調(diào)了記筆記和堅(jiān)持的重要性。這幾天也剛好在學(xué)習(xí)React,所以我打算每天堅(jiān)持一篇R...
    gaoer1938閱讀 1,813評(píng)論 0 5
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,568評(píng)論 19 139
  • 昨晚熬夜抑或加班,一大早到公司難免精神疲憊,哈欠連連。此時(shí),有的人開(kāi)始無(wú)所事事四處閑逛,有的人趴在桌上玩著手機(jī),有...
    馮凱源閱讀 854評(píng)論 0 0
  • React版本:15.4.2**翻譯:xiyoki ** 通常幾個(gè)組件需要響應(yīng)相同的數(shù)據(jù)變化。我們建議將共享狀態(tài)提...
    前端xiyoki閱讀 822評(píng)論 0 0
  • 這篇筆記主要包含 Vue 2 不同于 Vue 1 或者特有的內(nèi)容,還有我對(duì)于 Vue 1.0 印象不深的內(nèi)容。關(guān)于...
    云之外閱讀 5,177評(píng)論 0 29

友情鏈接更多精彩內(nèi)容