提升狀態

通常,幾個組件需要根據同一個數據變化做出響應。我們建議將這個共享的狀態提升到他們最近的一個共用祖先。讓我們看看實際該怎么做。
在這一節,我們將創建一個溫度計算器,用來計算一給定溫度能否讓水沸騰。
我們從名為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>
    );
  }
}

在CodePen上試一試

添加第二個輸入

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

現在我們可以改變Calculator來渲染兩個獨立的溫度輸入:

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

在CodePen上試一試
現在我們有兩個輸入了,但是當你在其中一個里輸入溫度,另一個不會去更新。這就不滿足我們的需求了,我們想讓他們同步。
我們也沒在Calculator中顯示BoilingVerdictCalculator不知道當前的溫度,因為溫度被隱藏在TemperatureInput中。

編寫轉換函數

首先我們寫兩個函數來互相轉換攝氏度和華氏溫度。

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

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

這兩個函數轉換數字。接下來我們寫另一個函數,接受一個temperature字符串和一個轉換函數作為參數,返回一個字符串。我們將用他來根據另一個input來計算這個input的值。
一個無效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組件都單獨地在本地狀態中保存自己的值:

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;

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

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

我們知道props是只讀的。之前temperature在本地狀態中,TemperatureInput只能調用this.setState()來改變它。而現在,temperature作為prop從父組件獲取,TemperatureInput不能再控制它了。
在React中,一般通過將組件變為“受控”,來解決這個。就像DOM<input>接受一個value和一個onChangeprop,所以自定義的TemperatureInput可以從它的父組件Calculator中獲取temperatureonTemperatureChangeprops。
現在,當TemperatureInput想要更新它的溫度值,調用this.props.onTemperatureChange就好了:

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

注意,自定義組件中的prop名字:temperatureonTemperatureChange并沒什么特別的意思。我們可以隨意命名,比如給它們更通用的名字valueonChange
父組件Calculator提供proponTemperatureChange的同時也提供temperature。他通過修改自己的本地狀態來處理更改,從而將兩個input重新渲染為新的值。我們馬上就來看看新的Calculator實現。
在深入Calculator的變化之前,我們來重新看遍TemperatureInput組件中做過什么變動。我們將他的本地狀態移除,將從this.state.temperature讀取,改為從this.props.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>
    );
  }
}

現在讓我們回到Calculator組件。
我們將當前輸入的temperaturescale存到他的本地狀態。這就是我們從input中“提升”的狀態,該狀態將作為“真相的來源”提供給兩個TemperatureInput。這是我們渲染兩個input,所需數據的最少表示。
假如,我們在攝氏度輸入框中輸入37,Calculator組件的狀態如下:

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

如果我們將華氏字段編輯為212,Calculator的狀態將變為:

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

我們可以存儲兩個輸入的值,但實際上是不必的。存儲最后一次變化的值和單位即可。然后我們可以根據當前的溫度和單位來換算出另一個值。
因為兩個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>
    );
  }
}

在CodePen上試一試
現在,不論你編輯哪一個input,Calculator中的this.state.temperaturethis.state.scale都會得到更新。從任何一個input獲取的用戶輸入都會被保存,另一個input的值會根據它重新計算。
讓我們重新看下當你編輯一個input時發生了什么:

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

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

經驗教訓

在React應用中,所有變化的數據都應該是單獨的“真相來源”。通常,狀態第一個被添加到組件中(組件需要用這些狀態來進行渲染)。如果其他組件也需要它,你可以將狀態提升到這些組件共用的最近祖先。你應該依賴自上而下的數據流,而不是同步多個組件的狀態。
比起雙向綁定的方法,提升狀態需要寫更多的“樣板”代碼,但好處就是,它可以更輕松的找到和隔離bug。因為任何狀態都是存在于組件中,并且只有組件可以修改它,由此bugs存在的區域大大被減少。另外,你可以實現任意邏輯來拒絕或轉換用戶的輸入。
如果某個值可以通過其他props或狀態獲得,那他就不該把他放在狀態中。比如,我們僅僅存儲上一次編輯的溫度和單位,而不是將celsiusValuefahrenheitValue都存下來。因為在render()方法中,一個input的值始終可以通過另一個計算得來。這樣,我們對另一個字段用或不用四舍五入,都不會在用戶的輸入中丟失精度。
當你發現UI上有錯誤發生,你可以使用React開發者工具來檢查props,然后沿著樹結構向上,知道你找到負責更新狀態的組件。這使你追溯到bug的來源:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容

  • 最近看了一本關于學習方法論的書,強調了記筆記和堅持的重要性。這幾天也剛好在學習React,所以我打算每天堅持一篇R...
    gaoer1938閱讀 1,709評論 0 5
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,830評論 18 139
  • 昨晚熬夜抑或加班,一大早到公司難免精神疲憊,哈欠連連。此時,有的人開始無所事事四處閑逛,有的人趴在桌上玩著手機,有...
    馮凱源閱讀 767評論 0 0
  • React版本:15.4.2**翻譯:xiyoki ** 通常幾個組件需要響應相同的數據變化。我們建議將共享狀態提...
    前端xiyoki閱讀 703評論 0 0
  • 這篇筆記主要包含 Vue 2 不同于 Vue 1 或者特有的內容,還有我對于 Vue 1.0 印象不深的內容。關于...
    云之外閱讀 5,070評論 0 29