Zustand 狀態管理筆記

AdSense

前言:為什麼選擇 Zustand?

Zustand (德語,意為「狀態」) 是一個輕量、快速且可擴展的 React 狀態管理庫。相較於傳統的 Redux 或 React Context API 搭配 useReducerzustand 提供了更簡潔、更接近 React Hook 哲學的狀態管理方案。

zustand 的核心優勢:

  • 極簡主義: 幾乎沒有樣板代碼 (Boilerplate),專注於狀態本身。
  • 非侵入式: 不需要 Context Provider 包裹應用程式,可以直接在任何組件中使用 Store Hook。
  • 基於 Hook: 狀態儲存本身就是一個 Hook,使用體驗與 useState 類似。
  • 自動優化: 內建機制支持精準的狀態選取,以避免不必要的渲染。

如何建立一個 Store

zustand 的核心是 create 函式,用於建立狀態儲存庫 (Store)。

定義 Store 結構

Store 是一個包含狀態 (State) 和動作 (Actions) 的物件。

src/stores/useCounterStore.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { create } from 'zustand';

// 定義狀態結構 (TypeScript)
// type CounterStore = {
// count: number;
// isSidebarOpen: boolean;
// increaseCount: (by: number) => void;
// resetCount: () => void;
// getFormattedCount: () => string;
// toggleSidebar: () => void;
// };

// create 接收一個函式,該函式會傳入 set (更新狀態) 和 get (取得狀態) 兩個引數
const useCounterStore = create((set, get) => ({
// 狀態 (State)
count: 0,
isSidebarOpen: false,

// 更改狀態的函式 (Actions)
// 使用 set 函式更新狀態時,建議傳入一個 updater 函式,確保基於最新狀態進行計算
increaseCount: by => set(prevState => ({ count: prevState.count + by })),

// 也可以直接傳入物件來覆蓋狀態,適用於簡單或重置操作
resetCount: () => set({ count: 0 }),

// 使用 get 函式讀取當前狀態,例如計算一個衍生值
getFormattedCount: () => {
const currentCount = get().count.toFixed(2);
return `當前計數值為:${currentCount}`;
},

toggleSidebar: () =>
set(prevState => ({ isSidebarOpen: !prevState.isSidebarOpen })),
}));

export default useCounterStore;

在 React 組件中使用 Store

Store 建立後,它就是一個可以被任何 React 組件直接調用的 Hook。

基礎選取,或分別選取:單一狀態或動作

zustand 的核心價值在於,它只在選取的狀態值發生變化時才重新渲染組件。

我們通過傳入一個選擇器函式 (Selector function) 給 Store Hook,來取得組件所需的特定狀態或動作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/components/CounterDisplay.jsx
import useCounterStore from '../stores/useCounterStore';

function CounterDisplay() {
// 選擇器:(state) => state.count
// 只有當 state.count 的值改變時,這個組件才會重新渲染
const count = useCounterStore((state) => state.count);

// 動作 (Action) 是一個函式,它的引用在 Store 建立後通常不會改變,
// 因此單獨選取 action 不會造成不必要的重新渲染
const increaseCount = useCounterStore((state) => state.increaseCount);

return (
<div>
<p>目前計數: {count}</p>
<button onClick={() => increaseCount(1)}>增加 (+1)</button>
</div>
);
}

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
2
import { useShallow } from 'zustand/react/shallow'; // 注意路徑
import useCounterStore from '../stores/useCounterStore';

步驟二:使用 useShallow 包裹選擇器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function OptimizedCounter() {
// 使用 useShallow,讓 zustand 對回傳的物件進行「淺層比較」
const { count, increaseCount, resetCount } = useCounterStore(
useShallow((state) => ({ // 將選擇器函式作為參數傳入 useShallow
// 只選擇您需要的所有狀態和動作
count: state.count,
increaseCount: state.increaseCount,
resetCount: state.resetCount,
}))
);

return (
<div>
<h2>優化後的計數: {count}</h2>
<button onClick={() => increaseCount(5)}>加五</button>
<button onClick={resetCount}>重置</button>
</div>
);
}
  • 運作原理: 選擇器回傳一個物件。useShallow 會對這個物件的所有屬性值進行淺層比較 (例如:比較 countincreaseCountresetCount 的值是否不同)。只要 countincreaseCountresetCount 都沒變,即使 Store 內有其他不相關的狀態變動 (例如 isSidebarOpen),這個組件也不會重新渲染。

2. 為什麼不能直接解構 useCounterStore()

一開始會想簡化語法,直接解構 Hook 的結果:

1
2
// 錯誤/不推薦的寫法 (可以動,但是會導致過度渲染 Over-rendering)
const { count, increaseCount, resetCount } = useCounterStore();

雖然這個寫法在功能上會運作,但它等同於使用了 (state) => state 作為選擇器,回傳了整個 Store 狀態物件。由於回傳的是整個物件,zustand 會對整個物件進行比較。

  • 問題: 如果 Store 內有其他不相關的狀態 (例如 isSidebarOpen) 發生變化時,由於訂閱了整個物件,zustand 會強制組件重新渲染,即使 countincreaseCountresetCount 都並未改變。這就是過度渲染 (Over-rendering),會損害應用程式性能。
  • 選擇器的作用: 選擇器的存在,就是為了精準篩選組件需要的狀態或函式,讓 zustand 只在該狀態或函式變動時才觸發組件更新。

處理異步操作 (Asynchronous Actions)

zustand 對異步操作的處理非常直覺。您只需在 Action 函式中執行您的異步邏輯 (例如 API 呼叫),然後在適當的時機呼叫 set 來更新狀態即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { create } from 'zustand';

const useAuthStore = create((set) => ({
user: null,
isLoading: false,
error: null,

login: async (username, password) => {
// 1. 開始載入,設定狀態
set({ isLoading: true, error: null });

try {
// 2. 模擬異步操作 (API 呼叫)
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
const data = await response.json();

// 3. 成功,更新 user 狀態
set({
user: data.user,
isLoading: false,
});

} catch (err) {
// 4. 失敗,設定錯誤狀態
set({
error: err.message,
isLoading: false,
user: null
});
}
},
}));

export default useAuthStore;