Zustand 狀態管理筆記
前言:為什麼選擇 Zustand?
Zustand (德語,意為「狀態」) 是一個輕量、快速且可擴展的 React 狀態管理庫。相較於傳統的 Redux 或 React Context API 搭配 useReducer,zustand 提供了更簡潔、更接近 React Hook 哲學的狀態管理方案。
zustand 的核心優勢:
- 極簡主義: 幾乎沒有樣板代碼 (Boilerplate),專注於狀態本身。
- 非侵入式: 不需要
Context Provider包裹應用程式,可以直接在任何組件中使用 Store Hook。 - 基於 Hook: 狀態儲存本身就是一個 Hook,使用體驗與
useState類似。 - 自動優化: 內建機制支持精準的狀態選取,以避免不必要的渲染。
如何建立一個 Store
zustand 的核心是 create 函式,用於建立狀態儲存庫 (Store)。
定義 Store 結構
Store 是一個包含狀態 (State) 和動作 (Actions) 的物件。
src/stores/useCounterStore.js
1 | import { create } from 'zustand'; |
在 React 組件中使用 Store
Store 建立後,它就是一個可以被任何 React 組件直接調用的 Hook。
基礎選取,或分別選取:單一狀態或動作
zustand 的核心價值在於,它只在選取的狀態值發生變化時才重新渲染組件。
我們通過傳入一個選擇器函式 (Selector function) 給 Store Hook,來取得組件所需的特定狀態或動作。
1 | // src/components/CounterDisplay.jsx |
useCounterStore 是一個 React Hook,它訂閱了 Counter Store 的更新通知 (用 create 建立出來的結構)。
zustand 監聽 Store 的變化。每當 Store 狀態更新時,它會重新執行您的選擇器,例:(state) => state.count。如果新回傳的 count 值與上一次的值不同 (例如 1 !== 0),那麼它就會更新 count 變數,並觸發組件的重新渲染。如果 count 沒變,但 Store 中的其他狀態變了,組件則不會重新渲染。
性能優化:選擇器的核心價值
1. 最佳實踐:合併選取與 useShallow
為了兼顧語法的簡潔性和性能的精準性,使用 useShallow Hook 來包裹合併選擇器。這是 zustand 官方在較新版本中推薦的最佳實踐,用以取代舊版將 shallow 作為第二個參數傳入的做法。
步驟一:引入 useShallow
1 | import { useShallow } from 'zustand/react/shallow'; // 注意路徑 |
步驟二:使用 useShallow 包裹選擇器
1 | function OptimizedCounter() { |
- 運作原理: 選擇器回傳一個物件。
useShallow會對這個物件的所有屬性值進行淺層比較 (例如:比較count、increaseCount和resetCount的值是否不同)。只要count、increaseCount和resetCount都沒變,即使 Store 內有其他不相關的狀態變動 (例如isSidebarOpen),這個組件也不會重新渲染。
2. 為什麼不能直接解構 useCounterStore()?
一開始會想簡化語法,直接解構 Hook 的結果:
1 | // 錯誤/不推薦的寫法 (可以動,但是會導致過度渲染 Over-rendering) |
雖然這個寫法在功能上會運作,但它等同於使用了 (state) => state 作為選擇器,回傳了整個 Store 狀態物件。由於回傳的是整個物件,zustand 會對整個物件進行比較。
- 問題: 如果 Store 內有其他不相關的狀態 (例如
isSidebarOpen) 發生變化時,由於訂閱了整個物件,zustand會強制組件重新渲染,即使count、increaseCount和resetCount都並未改變。這就是過度渲染 (Over-rendering),會損害應用程式性能。 - 選擇器的作用: 選擇器的存在,就是為了精準篩選組件需要的狀態或函式,讓
zustand只在該狀態或函式變動時才觸發組件更新。
處理異步操作 (Asynchronous Actions)
zustand 對異步操作的處理非常直覺。您只需在 Action 函式中執行您的異步邏輯 (例如 API 呼叫),然後在適當的時機呼叫 set 來更新狀態即可。
1 | import { create } from 'zustand'; |