React Zustand 輕量級狀態管理套件
在 React 開發中,狀態管理 (State Management) 一直是一個熱門話題。除了 React 內建的 Context API 和老牌的 Redux 之外,近年來 Zustand 異軍突起,成為 React 社群中最受歡迎的輕量級狀態管理庫之一。
Zustand(德語中的「狀態」)由 Poimandres 團隊開發,它的設計哲學是:簡單、極簡、去中心化。它解決了 Context API 可能導致的效能問題(不必要的重新渲染),同時也避免了 Redux 繁瑣的 boilerplate code。
為什麼選擇 Zustand?
- 極簡的 API:不需要 Provider 包裹整個 App,寫法非常直觀。
- 效能優化:透過 Selector 機制,元件只在實際上用到的資料改變時才會重新渲染。
- 非同步處理簡單:不需要像 Redux Thunk 或 Saga 這樣的中介軟體,直接在 action 中寫 async/await 即可。
- 輕量:打包後的大小非常小。
- DevTools 支援:支援 Redux DevTools,方便除錯。
安裝
使用 npm 或 yarn 安裝:
npm install zustand
基本用法
使用 Zustand 分為兩個步驟:
- 建立 Store:定義狀態 (State) 和操作狀態的方法 (Actions)。
- 在元件中使用:透過 Hook 的方式讀取狀態或呼叫方法。
1. 建立 Store
通常我們會建立一個獨立的檔案(例如 store.js 或 useStore.js)來定義 Store。Zustand 的 create 函式會回傳一個 Hook,通常命名為 use... 開頭。
import { create } from 'zustand';
// 建立一個存儲 "熊" 數量的 store
const useBearStore = create((set) => ({
bears: 0,
// 增加熊的數量
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
// 清空熊的數量
removeAllBears: () => set({ bears: 0 }),
}));
export default useBearStore;
2. 在元件中使用
現在你可以在任何元件中直接 import 這個 Hook 來使用狀態。
import useBearStore from './store';
function BearCounter() {
// 讀取狀態
const bears = useBearStore((state) => state.bears);
return <h1>目前有 {bears} 隻熊</h1>;
}
function Controls() {
// 讀取 actions
const increasePopulation = useBearStore((state) => state.increasePopulation);
return <button onClick={increasePopulation}>增加一隻熊</button>;
}
// 你的 App 不需要任何 Provider!
function App() {
return (
<>
<BearCounter />
<Controls />
</>
);
}
狀態更新 (Updating State)
在 create 函式中,你會獲得一個 set 函式。這個 set 函式是用來更新狀態的。
合併狀態 (Merging)
Zustand 預設會進行「淺層合併」(shallow merge)。這意味著你只需要提供想要改變的屬性即可,Zustand 會自動保留其他層級的屬性。
const useStore = create((set) => ({
firstName: 'John',
lastName: 'Doe',
updateFirstName: (name) => set({ firstName: name }), // lastName 會被保留
}));
依賴舊狀態 (Previous State)
如果新的狀態依賴於舊的狀態(例如計數器加一),set 可以接受一個 callback 函式,該函式的參數即為當前的 state。
const useStore = create((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
}));
讀取當前狀態 (Reading State via get)
在 create 方法中,除了 set 之外,你還可以接收 get 作為第二個參數。get() 函式允許你在 action 中讀取當前的 state。這在需要基於當前狀態進行邏輯判斷時(而非單純依賴前一個狀態更新)非常有用。
const useStore = create((set, get) => ({
count: 0,
action: () => {
const currentCount = get().count;
// 範例:只有當 count 小於 10 時才增加
if (currentCount < 10) {
set({ count: currentCount + 1 });
}
},
}));
非同步操作 (Async Actions)
Zustand 處理非同步操作非常簡單,你不需要額外的 middleware。只需要在 action 中使用 async/await,然後在完成後呼叫 set 即可。
const useDataStore = create((set) => ({
data: null,
loading: false,
error: null,
fetchData: async (url) => {
set({ loading: true, error: null }); // 開始載入
try {
const response = await fetch(url);
const data = await response.json();
set({ data: data, loading: false }); // 載入成功
} catch (error) {
set({ error: error.message, loading: false }); // 發生錯誤
}
},
}));
Selectors 與效能優化
這是 Zustand 最強大的功能之一。當你使用 useStore() 時,你可以傳入一個 selector 函式來只「選取」你需要的狀態部分。
元件只會在 selector 回傳的值發生改變時才會重新渲染。
好的做法 ✅
// ✅ 只有當 bears 改變時,這個元件才會重新渲染
const bears = useBearStore((state) => state.bears);
不好的做法 ❌
如果你不傳入 selector,預設會回傳整個 state 物件。雖然方便,但這意味著 store 中任何一個屬性改變,該元件都會重新渲染。
// ❌ store 中任何資料改變(例如 fish 改變),這個元件都會重新渲染
const { bears } = useBearStore();
取用多個狀態 (Shallow Check)
如果你想一次取用多個狀態,但又不想因為無關的狀態改變而渲染,可以使用 useShallow (需要 zustand v5) 或者自定義 equality function。
import { create } from 'zustand';
import { useShallow } from 'zustand/react/shallow';
const useStore = create((set) => ({
bear: 0,
fish: 0,
bird: 0,
}));
function Component() {
// 使用 useShallow,只有當 bear 或 fish 改變時才會渲染
// 如果 bird 改變,這個元件不會渲染
const { bear, fish } = useStore(useShallow((state) => ({ bear: state.bear, fish: state.fish })));
return (
<div>
Bear: {bear}, Fish: {fish}
</div>
);
}
處理巢狀物件 (Nested Objects)
雖然 Zustand 預設只做淺層合併,但更新深層巢狀物件也是可以的,只是需要寫比較多的 spread syntax (...)。
const useStore = create((set) => ({
deep: {
nested: {
obj: { count: 0 },
},
},
updateNested: () =>
set((state) => ({
deep: {
...state.deep,
nested: {
...state.deep.nested,
obj: {
...state.deep.nested.obj,
count: state.deep.nested.obj.count + 1,
},
},
},
})),
}));
immer middleware
為了簡化這個過程,推薦使用 Immer middleware,讓你可以用 mutable 的語法來更新 immutable 的狀態。
安裝:
npm install immer
使用 immer 更新巢狀狀態:
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
const useStore = create(
immer((set) => ({
deep: {
nested: {
obj: { count: 0 },
},
},
updateNested: () =>
set((state) => {
// 直接修改 draft state
state.deep.nested.obj.count += 1;
}),
})),
);
資料持久化 (Persist Middleware)
Zustand 內建了 persist middleware,可以幫你自動將 store 的狀態儲存到 localStorage、sessionStorage 或 AsyncStorage 中。當使用者重新整理頁面時,狀態會自動還原。
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
const useStore = create(
persist(
(set, get) => ({
bears: 0,
addABear: () => set({ bears: get().bears + 1 }),
}),
{
name: 'food-storage', // localStorage 中的 key 名稱 (必填)
storage: createJSONStorage(() => localStorage), // (選填) 預設就是 localStorage
},
),
);
除錯工具 (DevTools Middleware)
Zustand 支援 Redux DevTools Extension,這對於除錯非常有用。只需要用 devtools middleware 包裹你的 store 配置即可。
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
const useStore = create(
devtools((set) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 })),
})),
);
如果你同時使用 persist 和 devtools,建議將 devtools 放在最外層(或者根據具體需求調整,通常 devtools(persist(...)))。
在元件外部使用 (Outside Components)
有時候你可能需要在 React 元件以外的地方(例如 API 攔截器、工具函式)讀取或修改狀態。由於 Zustand store 本質上就是一個 hook 附帶著一些方法,你可以直接使用 getState 和 setState。
import useStore from './store';
// 1. 讀取狀態
const count = useStore.getState().count;
// 2. 修改狀態
useStore.setState({ count: 100 });
// 3. 訂閱狀態變化 (類似 useEffect)
const unsub = useStore.subscribe((state, prevState) => {
console.log('狀態改變了:', state);
});
// 取消訂閱
unsub();
大型應用程式架構 (Slice Pattern)
當你的 App 變得很大時,將所有的狀態和方法寫在一個檔案會變得很難維護。Zustand 推薦使用 Slice Pattern 來分割你的 store。
你可以建立多個 "Slice",每個 Slice 負責一部分的功能,最後再將它們合併成一個 Store。
// createBearSlice.js
const createBearSlice = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
});
// createFishSlice.js
const createFishSlice = (set) => ({
fishes: 0,
addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
});
// store.js
import { create } from 'zustand';
const useBoundStore = create((...a) => ({
...createBearSlice(...a),
...createFishSlice(...a),
}));
這樣你就可以將不同領域的邏輯拆分到不同的檔案中,保持程式碼清晰。
完整範例:待辦事項清單 (Todo List)
最後,我們來看一個完整的 Todo List 範例,包含了新增、切換狀態、刪除以及過濾功能。
// store.js
import { create } from 'zustand';
const useTodoStore = create((set) => ({
todos: [],
// 新增 Todo
addTodo: (text) =>
set((state) => ({
todos: [...state.todos, { id: Date.now(), text, completed: false }],
})),
// 切換完成狀態
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo,
),
})),
// 刪除 Todo
removeTodo: (id) =>
set((state) => ({
todos: state.todos.filter((todo) => todo.id !== id),
})),
}));
export default useTodoStore;
// App.jsx
import { useState } from 'react';
import useTodoStore from './store';
function TodoList() {
// 使用 selector 讀取 todos 和 methods
// 注意:這裡為了方便展示一次讀取,實務上如果擔心效能,可以分開讀取
const { todos, toggleTodo, removeTodo } = useTodoStore((state) => ({
todos: state.todos,
toggleTodo: state.toggleTodo,
removeTodo: state.removeTodo,
}));
return (
<ul>
{todos.map((todo) => (
<li key={todo.id} style={{ display: 'flex', gap: '10px', marginBottom: '5px' }}>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
cursor: 'pointer',
}}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>刪除</button>
</li>
))}
</ul>
);
}
function AddTodo() {
const [text, setText] = useState('');
const addTodo = useTodoStore((state) => state.addTodo);
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
addTodo(text);
setText('');
};
return (
<form onSubmit={handleSubmit}>
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="輸入代辦事項..." />
<button type="submit">新增</button>
</form>
);
}
export default function App() {
return (
<div>
<h1>Zustand Todo List</h1>
<AddTodo />
<TodoList />
</div>
);
}