Back To Articles

[React] 正確地使用 State 與 useState Hook

🧑🏻‍💻 海豹人 Sealman 📅 September 3, 2021

Article Image

本文介紹 React 中 State 的概念,包含 useState Hook、Snapshot、Lazy State Initialization 等觀念,看似簡單的狀態,其實有一些細節觀念是初學時容易忽略的。

初始化與重新渲染

useState - React 官方文件

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

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

這是 React 機制下所保證的,也是為什麼 useEffect 不必把 State 放入 Dependencies 的原因。

更改狀態

進行 setState 時,React 並不會馬上更改狀態,而是會進入排程,並且優先執行更重要的工作。

不過 React 還是會確保我們改變 State 的順序是正確的,例如以下有多個 setState,React 會按照排程順序執行狀態改變,因此 State 就會得到 Book。

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

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

取得狀態快照

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

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

特別是當新狀態的改變,是依賴於前一個狀態的時候,就要用這種 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