測驗:Zustand 基礎:極簡的 Client State 管理
共 5 題,點選答案後會立即顯示結果
1. 在 Zustand 中,create 函數的主要作用是什麼?
2. 以下哪一種是 Zustand 中 set 函數的正確用法?
3. 為什麼在元件中使用 selector 模式取得 Zustand store 的值是推薦做法?
4. 當你需要從 store 取出多個值並組成物件時,為什麼直接回傳物件會有問題?
5. 關於 Zustand 與 Context API、Redux 的比較,以下哪個敘述是正確的?
前言
在本系列的第一篇文章中,我們建立了「Client State 與 Server State 分離」的核心觀念。現在是時候深入探討 Client State 的管理方案了。
當你在閱讀 React 專案的程式碼時,可能會看到各種狀態管理的寫法:有些用 Redux 配合大量的 action、reducer 檔案;有些用 Context API 層層包裹 Provider;也有些用一個簡潔的 create 函數就搞定一切。
這篇文章要介紹的 Zustand,正是那個「簡潔搞定一切」的方案。當你在程式碼中看到 Zustand 的寫法時,你會發現它幾乎沒有學習曲線——如果你會用 useState,你就能讀懂 Zustand。
學習目標
讀完本篇後,你將能夠:
- 使用 Zustand 建立全域狀態 store
- 在 React 元件中存取和更新 Zustand 狀態
- 運用 selector 優化元件的重新渲染
Zustand 的核心概念:Store 就是一個 Hook
讓我們從一段最基礎的 Zustand 程式碼開始:
import { create } from 'zustand'
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}))
Code language: JavaScript (javascript)這段程式碼做了什麼?讓我們逐行解讀:
create 函數
create 是 Zustand 唯一需要記住的 API。它接收一個函數作為參數,這個函數會收到一個 set 參數,用來更新狀態。
create((set) => ({
// 這裡定義你的 state 和 actions
}))
Code language: JavaScript (javascript)State 與 Actions 一起定義
注意看 create 內部回傳的物件結構:
{
count: 0, // 這是 state
increment: () => set(...), // 這是 action
decrement: () => set(...), // 這也是 action
reset: () => set(...), // 這還是 action
}
Code language: CSS (css)Zustand 的哲學是:state 和 actions 放在一起。不需要分開寫 reducer、不需要定義 action types。想做什麼,直接寫一個函數就好。
set 函數的兩種用法
set 函數是更新狀態的唯一方式,它有兩種寫法:
// 寫法一:直接傳入新的部分狀態(會自動合併)
set({ count: 0 })
// 寫法二:傳入函數,可以存取當前狀態
set((state) => ({ count: state.count + 1 }))
Code language: JavaScript (javascript)當你需要根據「當前的值」來計算新值時,用寫法二;當你只是要設定一個固定值時,用寫法一。
在元件中使用 Store
建立好 store 之後,使用方式非常直覺:
function Counter() {
const count = useCounterStore((state) => state.count)
const increment = useCounterStore((state) => state.increment)
const decrement = useCounterStore((state) => state.decrement)
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}
Code language: JavaScript (javascript)這裡有幾個重點:
Store 就是一個 Hook
useCounterStore 本身就是一個 React Hook,可以直接在元件中呼叫。不需要包 Provider、不需要 useContext,直接用就好。
Selector 模式
注意我們傳入的箭頭函數:
useCounterStore((state) => state.count)
Code language: JavaScript (javascript)這就是所謂的 selector。它告訴 Zustand:「我只關心 count 這個值」。這不只是為了寫起來方便,更是效能優化的關鍵。
Selector 與效能優化
這是 Zustand 最重要的效能概念。讓我們看看為什麼 selector 如此重要。
錯誤示範:取出整個 store
// 不建議的寫法
function Counter() {
const store = useCounterStore() // 取出整個 store
return <span>{store.count}</span>
}
Code language: JavaScript (javascript)這樣寫的問題是:只要 store 中任何一個值改變,這個元件就會重新渲染。
正確做法:只訂閱需要的值
// 推薦的寫法
function Counter() {
const count = useCounterStore((state) => state.count)
return <span>{count}</span>
}
Code language: JavaScript (javascript)這樣寫,只有當 count 改變時,元件才會重新渲染。其他 state(例如 increment、decrement 函數)改變時,這個元件不會受影響。
多個值的情況
如果你需要取出多個值,有兩種正確的寫法:
方法一:多次呼叫 hook
function Counter() {
const count = useCounterStore((state) => state.count)
const increment = useCounterStore((state) => state.increment)
const decrement = useCounterStore((state) => state.decrement)
// ...
}
Code language: JavaScript (javascript)這是最推薦的寫法。每個 selector 獨立訂閱,只有相關的值改變時才會觸發重新渲染。
方法二:使用 useShallow
import { useShallow } from 'zustand/react/shallow'
function Counter() {
const { count, increment, decrement } = useCounterStore(
useShallow((state) => ({
count: state.count,
increment: state.increment,
decrement: state.decrement,
}))
)
// ...
}
Code language: JavaScript (javascript)useShallow 會對回傳的物件做淺比較(shallow comparison),避免每次都創建新的物件參考而觸發不必要的重新渲染。
常見的效能陷阱
以下是一個容易犯的錯誤:
// 有問題的寫法
const { count, increment } = useCounterStore((state) => ({
count: state.count,
increment: state.increment,
}))
Code language: JavaScript (javascript)這段程式碼看起來很合理,但每次 store 更新時,selector 都會回傳一個新的物件。由於物件比較是用 ===,新物件永遠不等於舊物件,所以元件會一直重新渲染。
解決方案就是前面提到的:用多次呼叫 hook,或是使用 useShallow。
實務範例:使用者偏好設定 Store
來看一個更實際的例子——管理使用者的 UI 偏好設定:
import { create } from 'zustand'
const usePreferencesStore = create((set) => ({
// State
theme: 'light',
language: 'zh-TW',
sidebarOpen: true,
// Actions
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
toggleSidebar: () => set((state) => ({
sidebarOpen: !state.sidebarOpen
})),
}))
Code language: JavaScript (javascript)在元件中使用:
function ThemeSwitcher() {
const theme = usePreferencesStore((state) => state.theme)
const setTheme = usePreferencesStore((state) => state.setTheme)
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
目前主題:{theme}
</button>
)
}
function Sidebar() {
const sidebarOpen = usePreferencesStore((state) => state.sidebarOpen)
const toggleSidebar = usePreferencesStore((state) => state.toggleSidebar)
if (!sidebarOpen) {
return <button onClick={toggleSidebar}>展開側邊欄</button>
}
return (
<aside>
<button onClick={toggleSidebar}>收合</button>
<nav>...</nav>
</aside>
)
}
Code language: JavaScript (javascript)注意這兩個元件完全獨立:
ThemeSwitcher只訂閱theme,側邊欄開關不會影響它Sidebar只訂閱sidebarOpen,主題切換不會影響它
這就是 selector 帶來的效能優勢。
存取 Actions:進一步優化
Actions(也就是那些更新狀態的函數)通常不會改變。我們可以利用這個特性進一步優化:
function ThemeSwitcher() {
const theme = usePreferencesStore((state) => state.theme)
// Actions 永遠不變,不需要放在元件內訂閱
const setTheme = usePreferencesStore.getState().setTheme
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
目前主題:{theme}
</button>
)
}
Code language: JavaScript (javascript)getState() 可以在元件外部取得當前的 store 狀態,適合用來取得永遠不變的 actions。
Zustand vs Redux vs Context API
最後,讓我們快速比較這三種方案,幫助你理解在程式碼中看到它們時的差異:
程式碼量比較
Context API:
// 需要:createContext + Provider + useContext
const ThemeContext = createContext()
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
function useTheme() {
return useContext(ThemeContext)
}
Code language: JavaScript (javascript)Redux(使用 Redux Toolkit):
// 需要:createSlice + configureStore + Provider + useSelector/useDispatch
const themeSlice = createSlice({
name: 'theme',
initialState: { value: 'light' },
reducers: {
setTheme: (state, action) => {
state.value = action.payload
},
},
})
const store = configureStore({
reducer: { theme: themeSlice.reducer },
})
Code language: JavaScript (javascript)Zustand:
// 就這樣
const useThemeStore = create((set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}))
Code language: JavaScript (javascript)何時選擇哪個?
| 方案 | 適合場景 |
|---|---|
| Context API | 少量、不常變動的狀態(如主題、語言設定) |
| Redux | 大型企業應用、需要嚴格的狀態管理規範、需要強大的 DevTools |
| Zustand | 中大型應用、追求簡潔與彈性、團隊希望快速迭代 |
對於大多數 2025 年的 React 專案,Zustand 配合 TanStack Query 已經成為主流選擇:Zustand 處理 Client State,TanStack Query 處理 Server State,各司其職,簡潔有力。
重點回顧
- create 函數:Zustand 的核心 API,回傳一個可直接使用的 Hook
- State 與 Actions 一起定義:不需要分開寫 reducer 和 action types
- set 函數:更新狀態的唯一方式,支援直接傳值或傳入函數
- Selector 模式:只訂閱需要的值,避免不必要的重新渲染
- useShallow:當需要取出多個值並組成物件時,用來做淺比較
- 不需要 Provider:這是 Zustand 最方便的特點之一
下一步
現在你已經理解了 TanStack Query(Server State)和 Zustand(Client State)的基礎。下一篇文章,我們將把這兩者結合在一起,看看在實際專案中如何優雅地同時運用這兩個工具。
參考資源
- Zustand 官方文件
- Zustand GitHub
- Prevent rerenders with useShallow – Zustand
- Auto Generating Selectors – Zustand
- Avoid performance issues when using Zustand – DEV Community
- State Management in 2025: When to Use Context, Redux, Zustand, or Jotai
進階測驗:Zustand 基礎:極簡的 Client State 管理
共 5 題,包含情境題與錯誤診斷題。