Section 1:你什麼時候需要 useReducer
很多開發者在學完 useState 之後,遇到複雜狀態就繼續硬撐,直到元件裡塞了 5、6 個 useState, 每個操作函式都要同時協調好幾個 state,才意識到事情不對勁。
先看一個具體的例子:用 useState 管理購物車,看看這有多痛苦。
這就是 useReducer 要解決的問題
5 個 useState,6 個操作函式,每個函式還要手動協調多個 state。 當邏輯繼續複雜化,這個元件將難以維護、難以測試、難以理解。
什麼時候該從 useState 升級到 useReducer?
| 用 useState | 用 useReducer |
|---|---|
| 單一簡單值 | 多個相關的 state |
| 操作邏輯簡單 | 多個操作共用複雜邏輯 |
| state 之間互相獨立 | state 之間有關聯 |
| 1–2 個操作 | 3+ 個操作 |
Section 2:useReducer 三角關係
useReducer 就像一個「郵差中心」的系統:
發出「訂單」— 呼叫 dispatch(action),說明你想做什麼操作。
根據「訂單類型」(action.type)決定怎麼改變倉庫,永遠回傳一個新的 state 物件。
儲存所有相關的狀態。更新後,React 重新渲染元件,畫面根據新倉庫狀態展示。
根據最新的 state 重新渲染,展示給用戶看。用戶操作又觸發新的 dispatch,形成循環。
Action 的型別定義(TypeScript 聯合型別)
在 TypeScript 中,用聯合型別(Union Type)定義所有可能的 action,讓 reducer 裡的 switch 可以做到完整的型別推導。
為什麼用聯合型別定義 Action?
當你在 switch 的每個 case 裡存取 action.payload, TypeScript 會根據 action.type 自動縮小型別(Type Narrowing)。 例如在 case 'ADD_ITEM' 裡, TypeScript 知道 payload 是 Product, 不是 { id: string }}。 這讓 reducer 的每個分支都能享有完整的型別安全。
Section 3:Reducer 函式
Reducer 是整個模式的核心。它接受「目前的 state」和「一個 action」,回傳「新的 state」。 所有的更新邏輯都集中在這裡,取代了分散在各個 handler 函式的 setXxx 呼叫。
Reducer 是純函式(Pure Function)
1. 相同輸入 = 相同輸出
輸入相同的 state + action,永遠輸出相同的新 state。沒有隨機性、沒有外部依賴。
2. 不能有副作用
不能在 reducer 裡 fetch API、不能 setTimeout、不能操作 DOM。副作用留給 useEffect 或 event handler。
3. 不修改原始 state
永遠回傳新物件(...state)。直接修改原始 state 不會觸發 React re-render。
Section 4:在元件中使用 useReducer
有了 reducer 函式和 action 型別定義,在元件裡呼叫 useReducer 非常簡潔。 所有操作函式都變成單一行的 dispatch 呼叫。
useReducer 讓元件職責變得清晰
元件(Cart)負責
- • 呼叫 useReducer 取得 state 和 dispatch
- • 根據 state 渲染 UI
- • 在事件裡 dispatch action
- • 處理非同步邏輯(fetch、try/catch)
Reducer 負責
- • 根據 action 計算新的 state
- • 所有「如何更新」的邏輯集中在此
- • 保持純函式,不做任何副作用
- • 可以獨立測試,不需要渲染任何 UI
Section 5:useReducer + Context — 全域狀態管理
useReducer 本身只解決「單一元件」內的複雜狀態問題。如果你希望購物車狀態在整個 App 都能存取, 做法是把 useReducer 放進 Context 裡。 這個組合被稱為「輕量 Redux」,在不引入第三方狀態管理庫的情況下, 實現全域可存取的結構化狀態。
這個模式解決了什麼?
集中的更新邏輯
所有狀態改變都透過 reducer,一眼就能看到所有可能的更新操作。
任何層級可存取
不需要 prop drilling,深層元件直接 useCart() 即可存取和更新狀態。
無額外依賴
純 React 內建 API,不需要安裝 Redux、Zustand 等外部套件。
Section 6:useReducer vs useState vs Zustand
選擇狀態管理工具沒有「最好」,只有「最適合當下場景」。以下是三者的對比,幫助你做出正確選擇。
| useState | useReducer | Zustand | |
|---|---|---|---|
| 學習成本 | 最低 | 低–中 | 低 |
| 適合場景 | 簡單值 | 複雜相關 state | 全域狀態 |
| TypeScript | 容易 | 很好(union type) | 很好 |
| Devtools | ❌ | ❌ | ✅ |
| Bundle size | 內建 | 內建 | 額外 2KB |
| 測試難度 | 簡單 | 最容易(純函式) | 簡單 |
useReducer 的隱藏優勢:測試超容易
因為 reducer 是純函式,你可以在完全不渲染任何 UI 的情況下,直接呼叫函式並驗證回傳值。 這是 useReducer 相比 useState 最大的測試優勢。
總結:如何選擇?
- useState— 獨立的簡單值、1–2 個操作。例如:modal 的 isOpen、input 的即時值。
- useReducer— 多個相關 state、3+ 個操作、需要可測試性。例如:購物車、表單狀態機、遊戲狀態。
- Zustand— 跨多個路由的全域狀態、需要 devtools、團隊規模較大。例如:用戶認證、主題設定、通知系統。