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);

但這三個狀態彼此其實有「順序關係」與「邏輯依賴」,例如儲存文章時會發生這樣的流程:

  1. 使用者按下儲存按鈕
  2. isSaving 變成 true → 顯示 loading 狀態
  3. 發送儲存 API 請求
  4. 請求成功 → isSaving 設回 falseerror 清空
  5. 請求失敗 → isSaving 設回 falseerror 設定錯誤訊息

雖然你可以這樣寫:

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),也就是:

  • 給它一組固定的輸入(stateaction),它永遠會產出相同的新狀態
  • 不會改動原本的 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:+1
  • decrement:-1
  • reset:重置為 0

你會發現:

  • 每一個 case 都代表一種「動作」
  • 它們的回傳值都是「新的狀態」
  • 而不是直接修改 state(這就是「不可變資料」的原則)

🧭 為什麼這樣設計比較好?

  1. 所有邏輯集中在一個函式內,條理清楚
  2. 每個 action 都像一張「指令單」,語意清楚可讀
  3. 未來要新增狀態或變化,只要加一個 case 就好
  4. 可以抽離 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 就會:

  1. 把目前的 state 和你送出的 action 傳進 reducer
  2. 用 reducer 的邏輯計算出新的 state
  3. 自動重新渲染元件畫面

📌 以計數器為例

假設你在按鈕的 onClick 中使用 dispatch

<button onClick={() => dispatch({ type: 'increment' })}>
  增加
</button>

這樣的行為就等於是在「告訴 reducer」:

「我要增加一個數字,請根據目前狀態處理並回傳新的值!」

而 reducer 則會執行:

case 'increment':
  return { count: state.count + 1 };

最終狀態就更新為 { count: 1 },畫面同步更新。

dispatch vs setState 的差異

行為useStateuseReducer
改變狀態的方式直接指定新值: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 };

延伸閱讀:理解程式設計中的「派發(Dispatch)」


useState vs useReducer 的差異比較

雖然 useStateuseReducer 都是 React 中用來「管理狀態」的 Hook,但它們在設計哲學與使用時機上有明顯差異。

以下用一張表格加上說明,幫助你快速搞懂它們各自的優缺點:

項目useStateuseReducer
適合場景✅ 適用於簡單、單一的狀態(如表單輸入、開關等)✅ 適合複雜狀態、多步驟流程、有關聯的狀態變化(如待辦清單、表單送出、多人狀態切換)
狀態變化邏輯✴️ 分散在多個 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:定義 initialStatereducer

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('');
  }
};

這段邏輯會在按下新增按鈕時:

  1. 檢查輸入是否為空
  2. 若有值,派發一個 add 的 action,將文字傳給 reducer
  3. 清空輸入欄位,重新渲染

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 控管

Similar Posts