1. 程式人生 > 實用技巧 >Avoiding React setState() Pitfalls(譯)

Avoiding React setState() Pitfalls(譯)

原文地址:Avoiding React setState() Pitfalls

我經常會閱讀到setState()是React中容易讓人誤解方面之一的相關的內容

考慮到管理元件狀態是React的一個基礎能力,我想了解一下有關使用setState()的常見陷阱和解決方案。

首先,快速概覽setState()和它的行為表現

setState()使用說明:

setState(updater, [callback]);

setState()接收兩個引數

  • 一個更新器
    • 一個物件(物件更新器)或者一個函式(函式更新器)
  • 一個回撥函式
    • setState() 是非同步的.
    • 元件狀態實際更新後將呼叫該回調

傳遞的更新器是一個物件時

  • 使用物件型別模式進行簡單的狀態更新
  • 傳遞的物件型別是簡單的
  // Using an object literal in setState

  this.setState({
    selectedLang: "Javascript"
  });

傳遞的更新器是一個函式時

  • 當更新需要引用以前的狀態時,請使用函式模式。
  • 傳遞更新物件是函式時可訪問prevState和當前props。
  • prevState是對先前狀態的引用。 它不應該直接突變。
    函式模式的使用說明
(prevState, props) => stateChange;

函式模式是基於傳入的prevState和props來建立一個新物件

// Using an updater function in setState to build a new object

this.setState((prevState, props) => {
  return { counter: prevState.counter + props.increment };
});

setState() 表現行為:

當setState()被呼叫時,它將完成兩件事:

  1. 排隊更改元件的狀態(它是非同步的)
    - 注意:如果傳入的是一個物件,React首先會將傳遞給setState()的物件合併到當前狀態
  2. 當狀態更新時,告訴React元件和它的子元件重新渲染
    - React的比對流程
    • 建立一個新的React元素樹(UI的物件表示)
    • 比較新樹和老樹的區別
    • 根據傳遞給setState()更新物件來確定更改的內容
    • 更新DOM
- 注意:在比對時,使用React[生命週期函式](https://facebook.github.io/react/docs/react-component.html#updating) 的不同階段執行程式碼

- [shouldComponentUpdate](https://facebook.github.io/react/docs/react-component.html#shouldcomponentupdate): 允許通過檢查先前狀態和新狀態來確定元件是否應更新自身。
  - 如果返回false,componentWillUpdate和componentDidUpdate不會執行。元件UI不會再渲染。
  - 在元件內,this.setState仍然會被更新。
- [componentWillUpdate](https://facebook.github.io/react/docs/react-component.html#componentwillupdate):在設定新狀態和進行渲染之前執行任何程式碼
- [render](https://facebook.github.io/react/docs/react-component.html#render): 視覺化地更新到DOM 
- [componentDidUpdate](https://facebook.github.io/react/docs/react-component.html#componentdidupdate): 新狀態設定完成並重新渲染元件後,執行程式碼。

setState()常見的陷阱

陷阱 1: 嘗試直接修改狀態

第一個錯誤,沒有使用setState()

  • 不要直接修改狀態
  • 直接修改this.state不會觸發元件重新渲染
  • this.state唯一應該存在的地方是元件的建構函式中
// 錯誤的寫法: 不會出發元件重新渲染

this.state.discount = false;

解決方案:使用setState()
setState() 會觸發元件的重新渲染.

// 正確的寫法: Use setState()

this.setState({
  discount: false
});

擴充套件:什麼時候應該把變數存放在元件的state裡?

  • 如果在render()裡沒有用到,this.state裡不應該存在

陷阱2: 嘗試同步使用setState

setState()是非同步的。 不要在一行上呼叫setState並預期該狀態在下一行上更改完成。

  • setState()是更新狀態的請求,而不是立即更新狀態的命令。
  • setState()並不總是立即更新元件。
// 錯誤的寫法: setState()不應該同步使用

// assuming this.state = { orders: 0 }
this.setState({
  orders: 1
});

console.log(this.state.orders); // BUG! Prints out: 0

有2中解決方案來解決這個問題:

  • 使用componentDidUpdate()生命週期方法(React團隊推薦)。
  • 定義一個回撥函式作為第二個引數傳遞給setState()。

使用componentDidUpdate或setState回撥(setState(updater,callback))可以確保您的程式碼將在應用狀態更新後執行。

解決方案 1: 使用生命週期函式componentDidUpdate()

發生更新後會立即呼叫componentDidUpdate()(但不會在Component的首次渲染上呼叫)。

// 正確的寫法: 使用生命週期函式 componentDidUpdate() 

componentDidUpdate(prevProps, prevState) {
  console.log(this.state.orders); // Prints out: 1
}

解決方案 2: 通過給setState()傳入一個回撥函式

setState()的第二個引數是可選的回撥函式,將在setState完成並重新渲染元件後執行。

// 正確的寫法: 將回調函式作為第2個引數傳遞給setState()

// assuming this.state = { orders: 0 }
this.setState(
  {
    orders: 1
  },
  () => {
    console.log(this.state.orders); // Prints out: 1
  }
);

陷阱 3: 嘗試使用stat儲存前一個狀態值

setState()是非同步的。 因此,不應將this.state的值用於計算下一個狀態。

  • this.props和this.state可能被非同步更新。
  • this.state不應用於計算下一個狀態。
// 錯誤的寫法: 不要依賴this.setState來計算下一個狀態值

this.setState({
  orders: this.state.orders + this.props.increment
});

解決方案: 使用函式更新器方式來獲取前一個狀態值

函式更新器的第一個引數,提供了對先前狀態的訪問:

函式更新器的用法

(prevState, props) => stateChange;
// 正確的寫法: 使用函式更新器方式來獲取前一個狀態值

this.setState((prevState, props) => ({
  orders: prevState.orders + props.increment
}));

陷阱 4: 嘗試發出多個setState()呼叫

同一週期內的多個setState()呼叫可能會被批量處理。 當向更新器傳遞物件時,這個問題尤為明顯。

  • setState()將更新程式物件淺合併到新狀態。
  • 後續的setState()呼叫將覆蓋同一週期中先前呼叫的值。
  • 如果更新器物件具有相同的keys,則傳遞給Object.assign()的最後一個物件的鍵值將覆蓋先前的值。
// 錯誤的寫法: 在同一週期內使用物件更新器多次呼叫setState(),將會被淺合併
// 假設 this.state = { orders: 0 };
this.setState({ orders: this.state.orders + 1});
this.setState({ orders: this.state.orders + 1});
this.setState({ orders: this.state.orders + 1});

// --> 輸出: this.state.orders 將會是 1, 不是我們預期的 3 

// 這等同於使用 Object.assign ,執行了一次淺合併
// orders只被增加了一次

Object.assign(
  previousState,
  {orders: state.orders + 1},
  {orders: state.orders + 1},
  {orders: state.orders + 1},
  ...
)

解決方案: 使用函式更新器來將狀態依次更新

通過向更新器傳入一個函式,需要更新的值將排隊更新,然後按呼叫順序執行。

//正確的寫法: 使用函式更新器來排隊更新狀態

// 假設 this.state = { orders : 0 };
this.setState(prevState => ({ orders: prevState.orders + 1 }));
this.setState(prevState => ({ orders: prevState.orders + 1 }));
this.setState(prevState => ({ orders: prevState.orders + 1 }));

// --> 輸出: this.state.orders 將會是 3