海豹人的第一個家

Sealman

Frontend Engineer / Taiwanese / Passion Comes From Mastery

Working with React State & useState Hook

本文介紹 React 中 State 的相關概念,包含 useState Hook、Snapshot、Lazy State Initialization 等觀念。

初始化與重新渲染

useState - React 官方文件

  • 初始化 (Initialization):State 只會在第一次載入時初始化,除非 DOM 移除,否則 useState 管理的 State 都不會重新初始化
  • 重新渲染 (Re-rendering):某狀態改變會導致元件重新繪製,不過只會更改該一狀態,其他狀態不會進行初始化(但是會執行初始化行為,算是小坑,本文最後會提到)

如果是第一次 Render,State 的值會是 initialState,但如果是後續的 Re-render,initialState 將不會被使用,State 會維持是最近一次更新的 State 值。

這是 React 保證的,也是為什麼後續 useEffect 不必把 State 放入 Dependencies。

更改狀態

進行 setState 時並不會馬上更改狀態,而是會進入排程,優先完成更重要的工作,不過 React 還是會確保我們改變 State 的順序是正確的。

例如:以下排程多個 setState,React 會到最後才按照排程順序執行狀態改變,結果 State 就會得到 Book。

const [state, setState] = useState('DVD');

setState('Pencil');
setState('Book'); // 最後的狀態會變成 Book

取得狀態快照

有時候我們需要馬上取得改變的結果,方便去做一些應用或計算。這時候我們可以透過「函式」的形式,讓狀態更新可以依賴於先前的狀態快照 (Snapshot)。

這樣我們就能得到基於狀態快照 (Snapshot) 的新狀態,而不是永遠都只會拿到最新的那一個狀態哩!

// 👇 Updating State That Depends On The Previous State.
// Safer way: Ensure the Latest state snapshot (If it depends on the previous state)
setUserInput((prevState) => {
  return { ...prevState, enteredTitle: event.target.value };
});

// Complex state: arrays
setUserInput((prevArray) => {
  return [...prevArray, { enteredTitle: event.target.value }];
});

// Complex state: objects
setUserInput((prevObject) => {
  return { ...prevObject, enteredTitle: event.target.value };
});

如果新狀態的改變是依賴於前一個狀態的話,就要用這種 Functional Return 的形式,以取得最新的狀態。

另外,除了上面這個方法,我們也可以借助 useEffect 來完成這個效果。useEffect 可以監聽 Dependencies 裡面的 State 有沒有改變,如果改變就執行 setState,因此也不會錯過狀態改變,能夠確保取得最新的狀態。

也因此,在 useEffect 裡使用的 setState 的時候,即使不是寫成函式的形式,也會即時更新狀態喔!

Lazy State Initialization

當一個元件裡的狀態被改變時,就會重新渲染那個元件,而在做 re-render 時 useState 的 initial value 雖然是沒用的,但是其實每次 re-render 時還是會再跑一次 initial value。

聽起來影響不大是嗎?畢竟沒影響到畫面呈現?

如果是在代價很低的情況下可能沒差,像是每次 re-render 都重新跑一次 console.log 或許無所謂。

const [state, setState] = React.useState(console.log('State initialization'));

但是如果初始 State 是一些需要複雜邏輯計算、效能耗費較高的狀態時,像是從 Local Storage 讀取值,那就必須考量到效能問題了。

const [state, setState] = React.useState(
  JSON.parse(localStorage.getItem('notes')) || []
);

這時候我們可以運用 useState 的一個特性,就稱之為懶惰的初始狀態好了,當我們想要避免執行 Initialize 時,就可以用這個 Lazy Initialization 的方法初始化 State。

寫法就是在 useState 原本傳入 value 的地方,改為傳入 Function 來 return 初始值。當傳入的是一個 Function 時,這個 Initial Function 就只會在初始 Render 時被調用執行,後續 Re-rendering 時不會再執行。

Lazily initialize our notes state so it doesn't reach into localStorage on every single re-render of the App component.

const [notes, setNotes] = React.useState(
  () => JSON.parse(localStorage.getItem('notes')) || []
);

Recap

  • State
  • useState Hook
  • Snapshot
  • Lazy State Initialization

References