You Probably Dont Need Derived State

原文連接: https://reactjs.org/blog/2018...
React 16.4包含了一個 getDerivedStateFromProps的 bug 修復:曾帶來一些 React 組件頻繁複現的 已有bug。若是你的應用曾經採用某種反模式寫法,可是在此次修復以後沒有被覆蓋到你的狀況,咱們對於該 bug 深感抱歉。在下文,咱們會闡述一些常見的, derived state相關的反模式,還有咱們的建議寫法。

很長一段時間,componentWillReceiveProps是響應props 改變,不會帶來額外從新渲染,更新 state 的惟一方式。在16.3版本中,咱們引入了一個生命週期方法getDerivedStateFromProps,爲的是以一種更安全的方式來解決一樣的問題。同時,咱們意識到人們對於這兩個鉤子函數的使用有許多誤解,也發現了一些形成這些晦澀 bug 的反模式。getDerivedStateFromProps的16.4版本修復使得 derived state更穩定,濫用狀況會減小一些。html

注意事項

本文說起的全部反模式案例面向舊鉤子函數 componentWillReceiveProps和新鉤子函數 getDerivedStateFromProps

本文會涵蓋下面討論:react

  • 何時去使用 derived state
  • 一些 derived state 的常見 buggit

    • 反模式:無條件地拷貝props 到state
    • 反模式:當 props 改變的時候清除 state
  • 建議解決方案
  • 內存化

何時去使用Derived State

getDerivedStateFromProps存在的惟一目的是使得組件在 props 改變時能都更新好內在state。咱們以前的博文有過一些例子,好比基於一個變化着的偏移 prop 來記錄當前滾動方向或者根據一個來源 prop 來加載外部數據。github

咱們沒有給出許多例子,由於整體原則上來說,derived state 應該用少點。咱們見過的全部derived state 的問題大多數能夠歸結爲,要麼沒有任何前提條件的從 props 更新state,要麼 props,state 不匹配的任什麼時候候去更新 state。(咱們將在下面談及更多細節)算法

  • 若是你正在使用 derived state 來進行一些基於當前 props 的內存化計算,那麼你不須要 derived state。memoization 小節會細細道來。
  • 若是你在無條件地更新 derived state或者 props,state 不匹配的時候去更新它,你的組件極可能太頻繁地重置 state,繼續閱讀可見分曉。

derived state 的常見 bug

受控,不受控概念一般針對表單輸入,可是也能夠用來描述組件的數據活動。props 傳遞進來的數據能夠當作受控的(由於父組件控制了數據源)。組件內部狀態的數據能夠當作不受控的(由於組件能直接改變他)。數組

最多見的derived state錯誤 就是混淆二者(受控,不受控數據);當一個 state 的變動字段也能夠經過 setState 調用來更新的時候,就沒有一個單一的(真相)數據源。上面談及的加載外部數據的例子可能聽起來狀況相似,可是一些重要方面仍是不同的。在加載例子中,source 屬性和 loading 狀態有着一個清晰數據源。當source prop改變的時候,loading 狀態老是被重寫。相反,loading 狀態只會在 prop 改變的時候被重寫,其餘狀況下就是被組件管控着。緩存

問題就是在這些約束變化的時候出現的。最典型的兩種形式以下,咱們來瞧瞧:安全

反模式: 無條件的從 props 拷貝至 state

一個常見的誤解就是覺得getDerivedStateFromPropscomponentWillReceivedProps會只在props 改變的時候被調用。實際上這兩個鉤子函數可能在父組件渲染的任什麼時候候被調用,無論 props 是否是和之前不一樣。所以,用這兩個鉤子函數來無條件消除 state 是不安全的。這樣作會使得 state 更新丟失。性能優化

咱們看看一個範例,這是一個郵箱輸入組件,鏡像了一個 email prop 到 state:app

class EmailInput extends Component {
  state = { email: this.props.email }

  render () {
    return <input onChange={this.handleChange} value={this.state.email} />
  }

  handleChange = e => {
    this.setState({ email: e.target.value })
  }

  componentWillReceiveProps(nextProps) {
    // This will erase any local state updates!
    // Do not do this.
    this.setState({ email: nextProps.email })
  }
}

剛開始,該組件可能看起來 Okay。State 依靠 props 來進行值初始化,咱們輸入的時候也會更新 State。可是若是父組件從新渲染的時候,咱們敲入的任何字符都會被忽略。就算咱們在 鉤子函數setState 以前進行了nextProps.email !== this.state.email的比較,也無濟於事。

在這個簡單例子中,咱們能夠經過增長shouldComponentUpdate,使得只在 email prop改變的時候從新渲染。可是實踐代表,組件一般會有多個 prop,另外一個 prop的改變仍舊可能形成從新渲染仍是有不正確的重置。函數和對象類型的 prop 常常行內生成。使得shouldComponentUpdate只容許在一種情形發生時返回 true很難實現。這兒有個直觀例子。因此,shouldComponentUpdate是性能優化的最佳手段,不要想着確保 derived state 的正確使用。

但願如今的你明白了爲何無條件拷貝 props 到 state 是個壞主意。在總結解決方案以前,咱們來看看相關反模式:若是咱們指向在 email prop 改變的時候去更新 state 呢

反模式: props 改變的時候擦除 state
接着上面例子繼續,咱們能夠避免在 props.email改變的時候故意擦除 state:

class EmailInput extends Component {
  state = {
    email: this.props.email
  }

  componentWillReceiveProps(nextProps) {
    // Any time props.email changes, update state.
    if (nextProps.email !== this.props.email) {
      this.setState({
        email: nextProps.email
      })
    }
  }
}

注意事項

即便上面的例子中只談到 componentWillReceiveProps, 可是也一樣適用於 getDerivedStateFromProps

咱們已經改善許多,如今組件會只在props 改變的時候清除咱們輸入過的舊字符。

可是還有一個殘留問題。想象一下一個密碼控件在使用上述輸入框組件,當涉及到擁有同一郵箱的兩個賬號的細節式,輸入框沒法重置。由於 傳遞給組件的prop值,對於兩個賬號而言是同樣的。這會困擾到用戶,由於一個帳號還沒保存的變動將會影響到共享同一郵箱的其餘賬號。這有demo

這是個根本性的設計失誤,可是也很容易犯錯,好比我。幸運的是有兩個更好的方案。關鍵在於,對於任何片斷數據,須要用一個單獨組件來保存數據,而且要避免在其餘組件重複。咱們來看看這兩個方案:

解決方案

推薦方案一:全受控組件

避免上面問題的一個辦法,就是從組件當中徹底移除 state。若是咱們的郵箱地址只是做爲一個 prop 存在,那麼咱們不用擔憂和 state 的衝突。甚至能夠把EmailInput轉換成一個更輕量的函數組件:

function EmailInput(props) {
  return <input onChange={props.onChange} value={props.email} />
}

這個辦法簡化了組件的實現,若是咱們仍然想要保存草稿值的話,父表單組件將須要手動處理。這有一個這種模式的demo

推薦方案二: 帶有 key 屬性的全不受控組件

另外一個方案就是咱們的組件須要徹底控制 draft 郵箱狀態值。這樣的話,組件仍然能夠接受一個prop初始值,可是會忽略該prop 的連續變化:

class EmailInput extends Component {
  state = { email: this.props.defaultEmail }

  handleChange = e => {
    this.setState({ email: e.target.value })
  }

  render () {
    return <input onChange={this.handleChange} value={this.state.email} />
  }
}

在聚焦到另外一個表單項的時候爲了重置郵箱值(好比密碼控件場景),咱們可使用React 的 key 屬性。當 key 變化時,React 會建立一個新組件實例,而不是更新當前組件。Keys 一般對於動態列表頗有用,不過在這裏也頗有用。在一個新用戶選中時,咱們用 user ID 來從新建立一個表單輸入框:

<EmailInput
  defaultEmail={this.props.user.email}
  key={this.props.user.id}
/>

每次 ID 改變的時候,EmailInput輸入框都會從新生成,它的 state 也就會重置到最新的 defaultEmail值。栗子不能少,這個方案下,沒有必要把 key 值添加到每一個輸入框。在整個form表單上 添加一個 key 屬性或許會更合理。每次 key 變化時,表單內的全部組件都會從新生成,同時初始化 state。

在大多數狀況,這是處理須要重置的state的最佳辦法。

注意事項

這個辦法可能聽起來性能慢,可是實際表現上可能微不足道。若是一個組件有複雜更新邏輯的話使用key屬性可能會更快,由於diffing算法走了彎路
  • 方案一:經過 ID 屬性重置 uncontrolled 組件

若是 key 因爲某個緣由不生效(有多是組件初始化成本高),那麼一個可用可是笨拙的辦法就是在getDerivedStateFromProps裏監聽userID 的變化。

class EmailInput extends Component {
  state = {
    email: this.props.defaulEmail,
    pervPropsUserID: this.props.userID,
  }

  static getDerivedFromProps(nextProps, prevState) {
    // Any time the current user changes,
    // Reset any parts of state that are tied to that user.
    // In this simple example, that's just the email.
    if (nextProps.userID !== prevState.prevPropsUserID) {
      return {
        prevPropsUserID: nextProps.userID,
        email: nextProps.defaultEmail,
      }
    }
    return null
  }

  // ...
}

若是這麼作的話,也給只重置組件部份內在狀態帶來了靈活性,舉個例子

注意事項

即便上面的例子中只談到 getDerivedStateFromProps, 可是也一樣適用於 componentWillReceiveProps
  • 方案二:用實例方法來重置非受控組件

極少狀況下,即便沒有用做 key 的合適 ID,你仍是想重置 state。一個辦法是把 key重置成隨機值或者每次你想重置的時候會自動糾正。另外一個選擇就是用一個實例方法用來命令式地重置內部狀態。

class EmailInput extends Component {
  state = {
    email: this.props.defaultEmail,
  }

  resetEmailForNewUser (newEmail) {
    this.setState({ email: newEmail })
  }

  // ...
}

父表單組件就能夠使用一個 ref 屬性來調用這個方法這裏有 Demo.

總結

總結一下,設計一個組件的時候,重要的是肯定數據是受控仍是不受控。

不要把 prop 值「鏡像」到 state,而是要讓組件受控,而且合併在一些父組件中的兩個分叉值。好比說,不是要讓子組件接收一個props.value,而且跟蹤一個草稿字段state.value,而是要讓父組件管理 state.draftValue還有state.committedValue,直接控制子組件的值。會使得數據流更明顯,更穩定。

對於不受控組件,若是你想要在一個 ID 這樣的特殊 prop 變化的時候重置 state,你會有如下選項:

  • 推薦:爲了重置全部內部state,使用 key 屬性
  • 方案一:爲了重置某些字段值,監聽一個props.userID這種特殊字段的變化
  • 方案二:也能夠會退到使用 refs 屬性的命令式實例方法

內存化

咱們已經看到 derived state 爲了確保一個用在 render的字段而在輸入框變化時被從新計算。這項技術叫作內存化

使用 derived state 去達到內存化並無那麼糟糕,可是也不是最佳方案。管理 derived state 自己比較複雜,屬性變多時變得更復雜了。好比說,若是咱們增長第二個 derived 字段到咱們的組件 state,那麼咱們須要針對兩個值的變化來作追蹤。

看看一個組件例子,它有一個列表 prop,組件渲染出匹配用戶查詢輸入字符的列表選項。咱們應該使用 derived state 來存儲過濾好的列表。

class Example extends Component {
  state = {
    filterText: '',
  }

  // ********************
  // NOTE: this example is NOT the recommended approach.
  // See the examples below for our recommendations instead.
  // ********************
  staitic getDerivedStateFromProps(nextProps, prevState) {
    // Re-run the filter whenever the list array or filter text change.
    // Note we need to store prePropsList and prevFilterText to detect change.
    if ( nextProps.list !== prevState.prevPropsList || prevState.prevFilterList !== prevState.filterText) {
      return {
        prevPropsList: nextProps.list,
        prevFilterText: prevState.filterText,
        filteredList: nextProps.list.filter(item => item.text.includes(prevState.filterText))
      }
    }
    return null
  }

  handleChange = e => {
    this.setState({ filterText: e.target.value })
  }

  render () {
    return (
      <Fragment>
        <input onChange={this.handleChange} value={this.state.filterText} />
        <ul>{this.state.filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
      </Fragment>
    )
  }
}

該實現避免了filteredList常常沒必要要的從新計算。可是也複雜了些。由於須要單獨追蹤 props和 state 的變化,爲的是適當的更新過濾好的列表。這裏,咱們可使用PureCompoennt來作簡化,把過濾操做放到 render 方法裏去:

// PureCompoents only rerender if at least one stae or prop value changes.
// Change is determined by doing a shallow comparison of stae and prop keys.
class Example Extends PureComponent {
  // State only needs to hold the current filter text value:
  state = {
    filterText: '',
  }

  handleChange = e => {
    htis.setState({ filterText: e.target.value })
  }

  render () {
    // The render method on this PureComponent is called only if
    // props.list or state.filterList has changed.
    const filteredList = this.props.list.filter(
      item => item.text.includes(this.stae.filterText)
    )

    return (
      <Fragment>
        <input onChange={this.handleChange} value={this.state.filterText} />
        <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
      </Fragment>
    )
  }
}

上面代碼要乾淨多了並且比 derived state 版本要更簡單。只是偶爾不夠好:對於大列表的過濾有點慢,並且若是另外一個 prop 要變化的話PureComponent不會防止從新渲染。基於這樣的考慮,咱們增長了memoization helper來避免非必要的列表從新過濾:

import memoize from 'memoize-one'

class Example extends Component {
  // State only need to hold the current filter text value:
  state = { filterText: '' }

  filter = memoize(
    (list, filterText) => list.filter(item => item.text.includes(filterText))
  )

  handleChange = e => {
    this.setState({ filterText: e.target.value })
  }

  render () {
    // Calculate the latest filtered list. If these arguments havent changed
    // since the last render, `'memoize-one` will reuse the last return value.
    const filteredList = this.filter(this.props.list, this.sate.filterText)

    return (
      <Fragment>
        <input onChange={this.handleChange} value={this.state.filterText} />
        <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
      </Fragment>
    )
  }
}

這要簡單多了,並且和 derived state 版本同樣好。

當使用memoization的時候,須要知足一些條件:

  1. 在大多數狀況下,你會把內存化函數添加到一個組件實例上。這會防止該組件的多個實例重置每個內存化屬性。
  2. 一般你使用一個帶有有限緩存大小的內存化工具,爲的是防止時間累計下來的內存泄露。(在上述例子中,咱們使用memoize-one由於它僅僅會緩存最近的參數和結果)。
  3. 這一節裏,若是每次父組件渲染的時候props.list從新生成的話,上述實現會失效。可是在多數狀況下,上述實現是合適的。

結束語

在實際應用中,組件常常混合着受控和不受控的行爲。理所應當。若是每一個值都有明確源,你就能夠避免上面的反模式。

重申一下,因爲比較複雜,getDerivedStateFromProps(還有 derived state)是一項高級特性,並且應該用少點。若是你使用的時候遇到麻煩,請在 GitHub 或者 Twitter 上聯繫咱們。

相關文章
相關標籤/搜索