useReducer:更強大的 state 管理方法
更新日期: 2025 年 4 月 19 日
當你開始寫 React 的時候,useState
絕對是最先學的 Hook。
它用起來直覺、方便,適合處理簡單的狀態更新。
但隨著應用越來越複雜,你會發現用 useState
管理多個狀態、或是多步驟流程時會變得難以維護,甚至常常改錯資料、讓 bug 悄悄溜進來。
這時候,useReducer
就能派上用場!
為什麼會需要 useReducer?
當我們開發應用程式時,往往不只是一個簡單的按鈕或文字輸入,狀態之間的邏輯關聯與流程控制才是讓程式變複雜的關鍵。
這時候,useState
可能會讓你陷入管理地獄。
多個 state 有邏輯關聯,卻分開管理?
用 useState
時,我們通常會針對每個資料定義一個獨立的 state:
const [title, setTitle] = useState('');
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState(null);
但這三個狀態彼此其實有「順序關係」與「邏輯依賴」,例如儲存文章時會發生這樣的流程:
- 使用者按下儲存按鈕
isSaving
變成true
→ 顯示 loading 狀態- 發送儲存 API 請求
- 請求成功 →
isSaving
設回false
、error
清空 - 請求失敗 →
isSaving
設回false
、error
設定錯誤訊息
雖然你可以這樣寫:
setIsSaving(true);
saveArticle(title)
.then(() => {
setIsSaving(false);
setError(null);
})
.catch((err) => {
setIsSaving(false);
setError(err.message);
});
但當專案越來越大,或這段邏輯被多次使用、重構時——
⚠️ 問題就來了:這些
setXXX
分散在各處,讓你很難追蹤某個狀態是在哪裡被改變的。
也就是說:
- 你得在好幾行程式中手動控制「誰先改、誰後改」
- 一但邏輯順序錯了,畫面就可能出現錯誤狀態(例如 loading 卡住、錯誤訊息顯示不正確)
多個 setState,容易讓邏輯變得混亂
設想一個稍微複雜一點的表單流程,需要管理以下狀態:
formData
(使用者輸入資料)isSubmitting
(是否正在送出)successMessage
(送出成功提示)errorMessage
(送出失敗提示)
如果你用 useState
,你就需要管理 4 個 state,再搭配流程控制邏輯(例如按鈕禁用、清除錯誤訊息),這些行為就散落在各個函式中,非常難以追蹤與維護。
解法:集中管理狀態變化
用 useReducer
,你就可以把所有狀態整合成一個 state 物件:
const initialState = {
title: '',
isSaving: false,
error: null,
};
並透過 dispatch({ type: 'some-action' })
這種方式來描述「我要做什麼」,而不是直接去設定每一個值。
範例如下:
function reducer(state, action) {
switch (action.type) {
case 'START_SAVE':
return { ...state, isSaving: true, error: null };
case 'SAVE_SUCCESS':
return { ...state, isSaving: false };
case 'SAVE_FAILURE':
return { ...state, isSaving: false, error: action.error };
default:
return state;
}
}
然後在元件中這樣使用:
dispatch({ type: 'START_SAVE' });
saveArticle(state.title)
.then(() => dispatch({ type: 'SAVE_SUCCESS' }))
.catch((err) =>
dispatch({ type: 'SAVE_FAILURE', error: err.message })
);
這樣有什麼好處?
- 邏輯集中:所有狀態變化都定義在
reducer
中,一目了然。 - 可維護性高:未來新增流程或狀態變化,只要加一個
case
就好。 - 更容易除錯與測試:你可以針對每個 action 測試它對 state 的影響,而不用考慮整個流程。
何時該考慮用 useReducer?
✅ 建議改用 useReducer
的時機:
- 狀態之間有邏輯或流程關聯(例如:步驟流程、表單狀態)
- 同一個動作會同時影響多個 state
- 你希望讓邏輯集中、好維護
- 想為未來導入 Redux 打好基礎
如果你發現自己的 useState
超過 3 個以上、還經常互相影響 —— 那 useReducer
就是你的好朋友了!
基本概念:什麼是 reducer?
在 React 中,reducer
是一種管理狀態變化的「純函式」。
它的作用是:根據你觸發的「動作(action)」來計算並回傳新的狀態(state)。
這個概念最早來自於狀態管理工具 Redux,而 React 也把這套做法內建成 Hook,叫做 useReducer
,讓你在不使用 Redux 的情況下,也能享有類似的集中管理邏輯。
✅ 如果
useState
是單打獨鬥,那useReducer
就像一間「中央調度室」,負責處理所有狀態的改變與邏輯判斷。
reducer 函式長什麼樣子?
reducer
本身是一個純函式(pure function),也就是:
- 給它一組固定的輸入(
state
和action
),它永遠會產出相同的新狀態 - 不會改動原本的
state
,而是回傳一份新的物件
它的基本結構是這樣:
function reducer(state, action) {
// 根據 action 內容,決定要怎麼改變 state
return newState;
}
🔹 參數一:state
(目前的狀態)
這是目前畫面上元件使用的資料,也就是 React 用來控制 UI 的依據。
可能是:
- 整數(像計數器:
{ count: 0 }
) - 字串(像表單輸入:
{ title: 'My Post' }
) - 或一個物件,裡面包含多個欄位(
{ name: '', isLoading: false, error: null }
)
你可以自由設計這份初始狀態的結構。
🔸 參數二:action
(觸發狀態改變的動作)
這是一個描述「我想做什麼」的物件,基本格式通常是:
{
type: '動作名稱'
}
最簡單的範例如下:
{
type: 'increment'
}
這表示「我希望狀態加一」。
你也可以傳更多資料進來(例如使用者輸入的表單內容):
{
type: 'setUser',
payload: {
name: 'PeiChun',
age: 30
}
}
這種結構非常有彈性,也讓 reducer 更容易擴充與維護。
範例:一個基本的 reducer 寫法
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
return state; // 沒有符合的動作,維持原樣
}
}
這段 reducer 專門負責處理一個「計數器」的狀態,支援三種動作:
increment
:+1decrement
:-1reset
:重置為 0
你會發現:
- 每一個
case
都代表一種「動作」 - 它們的回傳值都是「新的狀態」
- 而不是直接修改
state
(這就是「不可變資料」的原則)
🧭 為什麼這樣設計比較好?
- 所有邏輯集中在一個函式內,條理清楚
- 每個 action 都像一張「指令單」,語意清楚可讀
- 未來要新增狀態或變化,只要加一個 case 就好
- 可以抽離 reducer 做測試,不需要真的跑畫面
小提醒:reducer 是純函式!
reducer 的行為像數學公式一樣穩定,永遠不能有副作用(side effects)。
這代表:
- ❌ 不能寫
fetch()
、setTimeout()
、console.log()
等副作用 - ✅ 只能依據傳入的參數,計算並回傳新狀態
這種「只關心輸入與輸出」的做法,會讓你的程式邏輯更穩定、更容易 debug。
dispatch
是什麼?
上面我們介紹了 reducer 是如何根據 action
改變 state
,但你可能會想問——
「這個 reducer 函式是什麼時候被執行的?它怎麼知道該用哪個 action?」
答案就是:透過 dispatch
去「派發(dispatch)」一個動作,啟動 reducer 的運作。
在 useReducer
中,我們會這樣宣告 state:
const [state, dispatch] = useReducer(reducer, initialState);
這裡會得到兩個東西:
名稱 | 用途 |
---|---|
state | 當前的狀態(由 reducer 計算而來) |
dispatch | 一個函式,用來送出 action 給 reducer 處理 |
dispatch
的用途:派發 action,觸發狀態變化
當你希望變更狀態時,就要使用 dispatch()
來送出一個 action。
dispatch({ type: 'increment' });
這樣一來,React 就會:
- 把目前的
state
和你送出的action
傳進reducer
- 用 reducer 的邏輯計算出新的 state
- 自動重新渲染元件畫面
📌 以計數器為例
假設你在按鈕的 onClick
中使用 dispatch
:
<button onClick={() => dispatch({ type: 'increment' })}>
增加
</button>
這樣的行為就等於是在「告訴 reducer」:
「我要增加一個數字,請根據目前狀態處理並回傳新的值!」
而 reducer 則會執行:
case 'increment':
return { count: state.count + 1 };
最終狀態就更新為 { count: 1 }
,畫面同步更新。
dispatch
vs setState
的差異
行為 | useState | useReducer |
---|---|---|
改變狀態的方式 | 直接指定新值:setX() | 透過 dispatch() 送出 action |
狀態更新邏輯 | 分散在元件各處 | 集中在 reducer 中統一處理 |
修改多個狀態 | 多個 setX 交錯寫 | 一個 dispatch 對應完整的變化邏輯 |
可讀性與維護性 | 邏輯容易散亂 | 邏輯集中、可讀性高 |
✅ 小結:
dispatch
就是useReducer
的「觸發器」,只有透過它,你才能讓 reducer 執行、更新狀態。
dispatch 常見範例
👉 基本派發
dispatch({ type: 'toggle' });
👉 帶資料的 action(使用 payload)
dispatch({ type: 'setTitle', payload: '新標題' });
對應的 reducer:
case 'setTitle':
return { ...state, title: action.payload };
useState
vs useReducer
的差異比較
雖然 useState
和 useReducer
都是 React 中用來「管理狀態」的 Hook,但它們在設計哲學與使用時機上有明顯差異。
以下用一張表格加上說明,幫助你快速搞懂它們各自的優缺點:
項目 | useState | useReducer |
---|---|---|
適合場景 | ✅ 適用於簡單、單一的狀態(如表單輸入、開關等) | ✅ 適合複雜狀態、多步驟流程、有關聯的狀態變化(如待辦清單、表單送出、多人狀態切換) |
狀態變化邏輯 | ✴️ 分散在多個 setState 的地方,各自處理各自的更新 | ✅ 統一集中在 reducer 函式中,清楚定義「動作 → 狀態變化」 |
結構彈性 | 每一個狀態都需要獨立 useState,狀態多時難以管理 | 可以合併為一個 state 物件,集中設計與更新 |
可讀性與可維護性 | 一開始直覺簡單,但當狀態增加、流程複雜時變難維護 | 雖然初始學習曲線較高,但長期維護與擴充更穩定 |
重構難易度 | 拆解與合併狀態邏輯較麻煩,容易產生重複或錯誤邏輯 | 每個 action 對應明確邏輯,修改或擴充更容易 |
各項目詳解
✅ 適合場景
useState
適合用在「簡單的元件內部狀態」,例如輸入框內容、開關切換、modal 開關等。useReducer
則適合「一個動作會改變多個狀態」的情境,例如表單送出時要清空表單、顯示 loading、處理錯誤。
舉例比較:
狀態變化類型 | 使用 Hook 建議 |
---|---|
isOpen, count, username 獨立的幾個值 | 使用 useState |
formData, isSubmitting, error, step 需協同更新 | 使用 useReducer |
🔁 狀態變化邏輯
- 使用
useState
時,每個狀態都有自己的setXxx
函式,狀態變化分散在程式的各處。 - 使用
useReducer
時,所有狀態更新集中在reducer()
裡,邏輯一目了然,更容易 trace。
// useState
setIsLoading(true);
setError(null);
setFormData({});
// useReducer
dispatch({ type: 'RESET_FORM' });
🧱 結構彈性
useState
的缺點在於你有幾個狀態就要呼叫幾次 useState,而這些狀態彼此其實可能是關聯的。useReducer
則讓你能把多個狀態整合成一個物件,再根據 action 有組織地修改。
// useState(多個 state 拆開管理)
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [error, setError] = useState(null);
// useReducer(集中在一個 state 裡管理)
const initialState = {
name: '',
email: '',
error: null,
};
可讀性與可維護性
當功能還很簡單時,useState
的直覺性與快速上手是最大的優點。但當:
- 你的程式邏輯變複雜
- 狀態之間有明確的流程與關聯
- 同一段邏輯要處理多個狀態
這時候用 useReducer
不但讓你程式碼結構更清楚,也能防止意外邏輯錯誤,非常適合多人協作或大型專案。
選用時機建議
狀況 | 建議使用 |
---|---|
狀態少、變化單純(如開關、計數器) | useState |
狀態多、變化有流程或依賴(如表單送出、待辦清單) | useReducer |
想要集中管理邏輯、提高可維護性 | useReducer |
想學 Redux 前打基礎 | useReducer(結構非常類似) |
實作練習:用 useReducer
寫一個「待辦清單」
我們來實作一個簡單的 Todo List(待辦事項清單)應用程式,藉此練習如何使用 useReducer
管理多筆資料的增、改、刪邏輯。
這個範例會用到的 action.type
包括:
add
:新增一筆待辦事項toggle
:切換完成狀態remove
:刪除該筆項目
Step 1:定義 initialState
與 reducer
const initialState = [];
我們的初始狀態是空陣列,代表一開始沒有任何待辦事項。
接著定義 reducer 函式:
function reducer(state, action) {
switch (action.type) {
case 'add':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'toggle':
return state.map((item) =>
item.id === action.id ? { ...item, done: !item.done } : item
);
case 'remove':
return state.filter((item) => item.id !== action.id);
default:
return state;
}
}
✅ reducer 行為說明:
- add:使用者輸入的文字
action.text
,會被加進陣列中,並賦予唯一的id
。 - toggle:透過
map()
逐一檢查每個item
,如果id
符合就切換done
狀態(完成/未完成)。 - remove:使用
filter()
移除指定id
的項目。
Step 2:建立元件與管理輸入框
function TodoApp() {
const [todos, dispatch] = useReducer(reducer, initialState);
const [input, setInput] = useState('');
- 我們用
useReducer()
建立todos
狀態與dispatch
函式。 - 同時使用
useState()
管理使用者輸入的內容input
。
Step 3:新增待辦事項
const handleAdd = () => {
if (input.trim()) {
dispatch({ type: 'add', text: input });
setInput('');
}
};
這段邏輯會在按下新增按鈕時:
- 檢查輸入是否為空
- 若有值,派發一個
add
的 action,將文字傳給 reducer - 清空輸入欄位,重新渲染
Step 4:畫面渲染與按鈕操作
return (
<div>
<h2>📝 Todo List</h2>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="輸入待辦事項"
/>
<button onClick={handleAdd}>新增</button>
<ul>
{todos.map((todo) => (
<li
key={todo.id}
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
>
{todo.text}
<button onClick={() => dispatch({ type: 'toggle', id: todo.id })}>
✅
</button>
<button onClick={() => dispatch({ type: 'remove', id: todo.id })}>
❌
</button>
</li>
))}
</ul>
</div>
);
✅ 渲染重點:
- todos.map:根據目前的
todos
陣列逐一顯示每筆資料。 - toggle 按鈕:點一下就切換完成狀態(透過
dispatch(type: 'toggle')
)。 - remove 按鈕:點一下就刪除該項(透過
dispatch(type: 'remove')
)。 - 樣式提示:已完成的項目會加上刪除線(
textDecoration: 'line-through'
)。
最終完整程式碼:
import { useReducer, useState } from 'react';
const initialState = [];
function reducer(state, action) {
switch (action.type) {
case 'add':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'toggle':
return state.map((item) =>
item.id === action.id ? { ...item, done: !item.done } : item
);
case 'remove':
return state.filter((item) => item.id !== action.id);
default:
return state;
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(reducer, initialState);
const [input, setInput] = useState('');
const handleAdd = () => {
if (input.trim()) {
dispatch({ type: 'add', text: input });
setInput('');
}
};
return (
<div>
<h2>📝 Todo List</h2>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="輸入待辦事項"
/>
<button onClick={handleAdd}>新增</button>
<ul>
{todos.map((todo) => (
<li
key={todo.id}
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
>
{todo.text}
<button onClick={() => dispatch({ type: 'toggle', id: todo.id })}>
✅
</button>
<button onClick={() => dispatch({ type: 'remove', id: todo.id })}>
❌
</button>
</li>
))}
</ul>
</div>
);
}
延伸閱讀:
學習重點總整理
重點 | 說明 |
---|---|
useReducer | 適合用來管理陣列資料、複雜邏輯、多動作處理 |
action.type | 用來分類要做的事(新增、刪除、切換) |
dispatch | 將指令送進 reducer 的關鍵函式 |
reducer | 根據不同 action 決定要怎麼處理狀態 |
state | 所有待辦資料都存在這裡,由 reducer 控管 |