【React】為什麼我不再使用setState?

繆宇發表於2017-10-12

幾個月前,我開始停止使用React的 setState 。我並不是不再需要元件狀態,而且不再用React來管理我的元件狀態。

setState對於新手來說不是很友好,即使是有經驗的React程式設計師在使用setState時,也很容易出bug,比如:

Bug產生的原因是忘記了React的state是非同步的;從日誌列印延遲可以看出來。

React的官方文件已將把使用setState可能會出現的所有問題都總結了:

注意:

永遠不要直接修改this.state,需要通過呼叫this.setState方法來替換你修改後的值。把this.state當做不可變資料來處理。

setState()不會馬上去改變this.state,而是會排隊等待處理,所以當你呼叫setState()後訪問this.state,有可能會返回舊的state

當你呼叫setState()時,無法保證是同步執行的,因為為了保證效能可能會被批處理。

setState()總是會觸發render()進行重新渲染,除非在shouldComponentUpdata()控制了渲染邏輯。如果用了可變資料結構以及在shouldComponentUpdata()中並沒有控制渲染邏輯,呼叫setState()將不會觸發重新渲染渲染。

總的來說,使用setState會帶來三個問題:

1. setState是非同步的

許多開發人員起初並沒有意識到這一點,當你設定了新的state,卻發現沒有變化,這是setState最詭異的地方,因為setState呼叫的時候看起來並不是非同步。比如下面的程式碼:


class Select extends React.Component {

  constructor(props, context) {

    super(props, context)

    this.state = {

      selection: props.values[0]

    };

  }

  render() {

    return (

      <ul onKeyDown={this.onKeyDown} tabIndex={0}>

        {this.props.values.map(value =>

          <li

            className={value === this.state.selection ? 'selected' : ''}

            key={value}

            onClick={() => this.onSelect(value)}

          >

            {value}

          </li> 

        )}  

      </ul>

    )

  }

  onSelect(value) {

    this.setState({

      selection: value

    })

    this.fireOnSelect()

  }

  onKeyDown = (e) => {

    const {values} = this.props

    const idx = values.indexOf(this.state.selection)

    if (e.keyCode === 38 && idx > 0) { /* up */

      this.setState({

        selection: values[idx - 1]

      })

    } else if (e.keyCode === 40 && idx < values.length -1) { /* down */

      this.setState({

        selection: values[idx + 1]

      })  

    }

    this.fireOnSelect()

  }

  fireOnSelect() {

    if (typeof this.props.onSelect === "function")

      this.props.onSelect(this.state.selection) /* not what you expected..*/

  }

}

ReactDOM.render(

  <Select 

    values={["State.", "Should.", "Be.", "Synchronous."]} 

    onSelect={value => console.log(value)}

  />,

  document.getElementById("app")

)複製程式碼

乍一看沒什麼問題,但是這個select元件有一個bug,上面的git圖已經很好的證明了。onSelect()方法觸發時總是得到前一個state.selection的值,因為setState還沒有完成,fireOnSelect就被呼叫了。我認為應該把setState重新命名為scheduleState或者要求傳入回撥。

這個bug很容易修復,棘手的地方在於你很難發現它。

2. setState引起沒有必要的渲染

setState的第二個問題在於它總是會觸發重新渲染,很多時候這種渲染是沒有必要的。你可以通過printWasted(React提供的效能工具)來檢測它會在什麼時候重新渲染。從三個方面來粗略的講為什麼重新渲染有時候是沒有必要的:

  • 當新的state和舊的state是一樣的。可以通過shouldComponentUpdate來解決,也可以用純渲染庫來解決。

  • 只有某些時候state的改變才和渲染有關係。

  • 第三,某些時候state和檢視層一點關係都沒有,比如用來管理事件的監聽器,定時器等相關的state

3. setState不可能管理所有元件的狀態

接著上面最後那點說,不是所有的元件狀態都需要通過setState來儲存和更新。大多數複雜的元件通常需要管理定時器迴圈,介面請求,事件等等。如果用setState來管理,不僅僅會引起沒有必要渲染,而且會造成死迴圈。

用MobX來管理元件狀態

程式碼如下:


import {observable} from "mobx"

import {observer} from "mobx-react"

@observer class Select extends React.Component {

  @observable selection = null; /* MobX managed instance state */

  constructor(props, context) {

    super(props, context)

    this.selection = props.values[0]

  }

  render() {

    return (

      <ul onKeyDown={this.onKeyDown} tabIndex={0}>

        {this.props.values.map(value =>

          <li

            className={value === this.selection ? 'selected' : ''}

            key={value}

            onClick={() => this.onSelect(value)}

          >

            {value}

          </li> 

        )}  

      </ul>

    )

  }

  onSelect(value) {

    this.selection = value

    this.fireOnSelect()

  }

  onKeyDown = (e) => {

    const {values} = this.props

    const idx = values.indexOf(this.selection)

    if (e.keyCode === 38 && idx > 0) { /* up */

      this.selection = values[idx - 1]

    } else if (e.keyCode === 40 && idx < values.length -1) { /* down */

      this.selection = values[idx + 1]

    }

    this.fireOnSelect()

  }

  fireOnSelect() {

    if (typeof this.props.onSelect === "function")

      this.props.onSelect(this.selection) /* solved! */

  }

}

ReactDOM.render(

  <Select 

    values={["State.", "Should.", "Be.", "Synchronous."]} 

    onSelect={value => console.log(value)}

  />,

  document.getElementById("app")

)複製程式碼

效果如下:

用同步的元件狀態的機制沒有出現意想不到的bug。

上面的程式碼片段不僅簡潔美觀,MobX還解決了setState的全部問題:

改變state馬上就反映到了元件state上。這讓程式碼邏輯和程式碼重用變得更簡單。你不用去擔心state是否更新了。

MobX在執行時候確定哪些state與檢視層關聯,暫時與檢視層無關的State不會引起重新渲染,直到再次關聯。

所以可渲染和不可渲染狀態是統一處理的。

此外,直接修改state物件的低階錯誤不能再犯了。還有,不要擔心是否還能用shouldComponentUpdate 和或者PureRenderMixin方法,MobX也很好的處理了。最後,你也許會想,我如何等到setState處理完成呢,你可以使用compentDidUpdate生命週期。

最後:

我已經停止使用React來管理元件狀態了,而是用MobX代替。現在React成了“正真”的檢視層:)。

以下是實現的Dome:

原文:medium.com/@mweststrat…

相關文章