【TanStack Query + Zustand 教學】#04 Zustand 基礎:極簡的 Client State 管理

測驗:Zustand 基礎:極簡的 Client State 管理

共 5 題,點選答案後會立即顯示結果

1. 在 Zustand 中,create 函數的主要作用是什麼?

  • A. 建立一個 React Context
  • B. 建立一個 Redux reducer
  • C. 建立一個可直接在元件中使用的 Hook(store)
  • D. 建立一個需要包 Provider 的狀態容器

2. 以下哪一種是 Zustand 中 set 函數的正確用法?

// 選項 A set((state) => ({ count: state.count + 1 })) // 選項 B set({ count: 0 }) // 選項 C set.count = 5 // 選項 D set(() => this.count++)
  • A. 選項 A 和 B 都是正確的
  • B. 只有選項 A 是正確的
  • C. 只有選項 B 是正確的
  • D. 選項 C 和 D 都是正確的

3. 為什麼在元件中使用 selector 模式取得 Zustand store 的值是推薦做法?

// Selector 模式 const count = useCounterStore((state) => state.count)
  • A. 這樣寫程式碼比較短
  • B. 只有當訂閱的特定值改變時,元件才會重新渲染
  • C. 這是 React 官方要求的寫法
  • D. 這樣可以避免使用 Provider

4. 當你需要從 store 取出多個值並組成物件時,為什麼直接回傳物件會有問題?

// 有問題的寫法 const { count, increment } = useCounterStore((state) => ({ count: state.count, increment: state.increment, }))
  • A. 這樣會導致記憶體洩漏
  • B. Zustand 不支援解構賦值
  • C. 每次 store 更新時,selector 都會回傳新物件,導致元件一直重新渲染
  • D. 這樣無法正確取得 increment 函數

5. 關於 Zustand 與 Context API、Redux 的比較,以下哪個敘述是正確的?

  • A. Zustand 必須使用 Provider 包裹元件才能使用
  • B. Zustand 需要分開定義 reducer 和 action types
  • C. Context API 比 Zustand 更適合中大型應用的狀態管理
  • D. Zustand 可以不需要 Provider,直接在元件中使用 Hook

前言

在本系列的第一篇文章中,我們建立了「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(例如 incrementdecrement 函數)改變時,這個元件不會受影響。

多個值的情況

如果你需要取出多個值,有兩種正確的寫法:

方法一:多次呼叫 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,各司其職,簡潔有力。

重點回顧

  1. create 函數:Zustand 的核心 API,回傳一個可直接使用的 Hook
  2. State 與 Actions 一起定義:不需要分開寫 reducer 和 action types
  3. set 函數:更新狀態的唯一方式,支援直接傳值或傳入函數
  4. Selector 模式:只訂閱需要的值,避免不必要的重新渲染
  5. useShallow:當需要取出多個值並組成物件時,用來做淺比較
  6. 不需要 Provider:這是 Zustand 最方便的特點之一

下一步

現在你已經理解了 TanStack Query(Server State)和 Zustand(Client State)的基礎。下一篇文章,我們將把這兩者結合在一起,看看在實際專案中如何優雅地同時運用這兩個工具。


參考資源

進階測驗:Zustand 基礎:極簡的 Client State 管理

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你需要建立一個購物車 store,追蹤商品數量並提供增加、減少、清空功能。以下哪個實作方式最符合 Zustand 的最佳實踐? 情境題

  • A. 建立三個獨立的 store 分別管理 count、increment、decrement
  • B. 用一個 create 函數建立 store,state 和 actions 定義在同一個物件中
  • C. 使用 Context API 搭配 useReducer 來管理
  • D. 分別建立 cartSlice 和 cartActions,然後用 configureStore 整合

2. 同事寫了以下程式碼,但發現即使只有 sidebarOpen 改變,ThemeSwitcher 元件也會重新渲染。問題出在哪裡? 錯誤診斷

function ThemeSwitcher() { const store = usePreferencesStore() // 取出整個 store return ( <button onClick={() => store.setTheme(store.theme === ‘light’ ? ‘dark’ : ‘light’)}> 目前主題:{store.theme} </button> ) }
  • A. setTheme 函數沒有正確定義
  • B. 應該使用 useContext 來取得 store
  • C. 取出整個 store 會導致 store 中任何值改變都觸發重新渲染,應該用 selector 只訂閱 theme
  • D. 三元運算子的邏輯寫反了

3. 你在開發一個設定頁面,需要同時顯示並更新 themelanguagefontSize 三個設定值。為了避免效能問題,最佳做法是? 情境題

  • A. 呼叫三次 hook,每次用 selector 取一個值
  • B. 用一個 selector 回傳包含三個值的物件
  • C. 直接呼叫 useSettingsStore() 不帶參數取出整個 store
  • D. 建立三個獨立的 store 分別管理

4. 以下程式碼想要實作「根據當前值更新狀態」的功能,但有一個潛在問題。哪個選項正確指出問題? 錯誤診斷

const useCounterStore = create((set) => ({ count: 0, doubleCount: () => set({ count: count * 2 }), // 有問題 }))
  • A. doubleCount 應該改名為 double
  • B. count 在 action 中無法直接存取,應該用 set((state) => ({ count: state.count * 2 }))
  • C. set 函數不支援乘法運算
  • D. 應該用 set({ count: this.count * 2 })

5. 你的應用程式有個全域 notification store,多個元件都會呼叫 showNotification action。為了優化效能,你想讓 action 不會觸發訂閱該 action 的元件重新渲染。最佳做法是? 情境題

// 目前的寫法 function Header() { const showNotification = useNotificationStore((state) => state.showNotification) // … }
  • A. 使用 useMemo 包裝 showNotification
  • B. 將 showNotification 放到 Context 中傳遞
  • C. 使用 useNotificationStore.getState().showNotification 在元件外部取得 action
  • D. 每次呼叫時重新建立 store

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *