React 初心者大補帖(5)

使用 useReducer 的正確姿勢

useReducer 是什麼?

想像你有一個玩具盒:

  • useState:直接把玩具丟進盒子裡,想要什麼就直接拿什麼
  • useReducer:你有一個聰明的機器人管家,你跟機器人說「我要做什麼」,機器人就幫你整理玩具盒

舉個例子:管理零用錢

useState 方式(自己管理每件事)

1
2
3
4
5
// 就像直接數錢包裡的錢
const [money, setMoney] = useState(100);

// 想買東西就直接減錢
setMoney(money - 50); // 買了50元的東西

useReducer 方式(機器人管家)

1
2
3
4
5
6
7
8
// 告訴機器人:「我要買東西」
dispatch({ type: "買東西", price: 50 });

// 機器人會自動幫你:
// 1. 檢查錢夠不夠
// 2. 記錄你買了什麼
// 3. 算出剩下多少錢
// 4. 整理好你的帳本

基本語法和概念

1
2
3
4
5
6
const [state, dispatch] = useReducer(reducer, initialState);

// reducer: 純函數,接收當前 state 和 action,返回新的 state
// initialState: 初始狀態值
// state: 當前狀態
// dispatch: 觸發狀態更新的函數

什麼時候需要機器人管家?

  • 事情很複雜時:要記錄很多東西(零用錢、存款、借錢記錄)並做很多計算,如果散開來寫很容易造成閱讀上的困難
  • 避免搞錯:不會忘記記帳、不會算錯錢、按照固定步驟做,自行處理的話很容易在龐大的邏輯中迷失,漏掉一些驗證

管理零用錢

讓我們用程式碼來看四個步驟的差異:

  1. 檢查錢夠不夠
  2. 記錄你買了什麼
  3. 算出剩下多少錢
  4. 整理好你的帳本

useState 方式(自己管理每件事)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function MoneyManagerWithUseState() {
  const [money, setMoney] = useState(100);
  const [history, setHistory] = useState<string[]>([]);
  const [error, setError] = useState("");
  const [lastPurchase, setLastPurchase] = useState("");
  
  const buyThing = (itemName: string, price: number) => {
    // 1. 檢查錢夠不夠
    if (money < price) {
      setError("錢不夠!");
      return;
    }
    setError("");
    
    // 2. 記錄你買了什麼
    setHistory(prev => [...prev, `買了${itemName} -${price}元`]);
    setLastPurchase(itemName);
    
    // 3. 算出剩下多少錢
    setMoney(prev => prev - price);
    
    // 4. 整理好你的帳本
    console.log("帳本更新完成");
  };
  
  return (
    <div>
      <p>剩餘金額{money}</p>
      <p>最後購買{lastPurchase}</p>
      <p>錯誤訊息{error}</p>
      <button onClick={() => buyThing("玩具車", 30)}>
        買玩具車 (30)
      </button>
    </div>
  );
}

useReducer 方式(機器人管家)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 機器人管家的「工作手冊」
function moneyReducer(state, action) {
  switch (action.type) {
    case "買東西":
      const { itemName, price } = action;
      
      // 1. 檢查錢夠不夠
      if (state.money < price) {
        return { ...state, error: "錢不夠!" };
      }
      
      // 2. 記錄你買了什麼
      const newHistory = [...state.history, `買了${itemName}-${price}元`];
      
      // 3. 算出剩下多少錢
      const newMoney = state.money - price;
      
      // 4. 整理好你的帳本
      return {
        money: newMoney,
        history: newHistory,
        lastPurchase: itemName,
        error: "",
        totalSpent: state.totalSpent + price
      };
      
    default:
      return state;
  }
}

function MoneyManagerWithUseReducer() {
  const [state, dispatch] = useReducer(moneyReducer, {
    money: 100,
    history: [],
    lastPurchase: "",
    error: "",
    totalSpent: 0
  });
  
  const buyThing = (itemName: string, price: number) => {
    // 只要跟機器人說:「我要買東西」
    dispatch({ type: "買東西", itemName, price });
  };
  
  return (
    <div>
      <p>剩餘金額{state.money}</p>
      <p>最後購買{state.lastPurchase}</p>
      <p>總共花費{state.totalSpent}</p>
      <p>錯誤訊息{state.error}</p>
      <button onClick={() => buyThing("玩具車", 30)}>
        買玩具車 (30)
      </button>
    </div>
  );
}

看著變長的程式碼,你的心裡會不會浮現一個疑問:

「等等,這樣真的比較好嗎? 如果我只在一個地方使用,直接用 useState 寫在那裡不是比較簡單嗎?」

什麼時候 useState 比較好?

如果只是簡單的計數器,而且只在一個地方使用的話,直接用 useState 寫就好,不需要炫技,把程式碼變得複雜。

什麼時候 useReducer 才真正有價值?

  • 邏輯會被重複使用:像是購物車的邏輯可以在商品頁面、購物車頁面、結帳頁面使用,這時候就可以寫成 reducer
  • 邏輯真的很複雜
  • 需要測試邏輯

真正複雜的情境:智慧零用錢管理系統

讓我們擴展剛才的例子,讓小明不只管零用錢,還要:

  • 管理多個錢包(現金、存款、紅包錢)
  • 設定預算限制
  • 記錄收入和支出
  • 自動儲蓄規則
  • 達成目標獎勵

如果使用 useState 會變成災難:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function MoneyManagerComplex() {
  
  // 一卡車 state...
  const [cash, setCash] = useState(100);
  const [savings, setSavings] = useState(500);
  const [redEnvelope, setRedEnvelope] = useState(200);
  const [dailyBudget, setDailyBudget] = useState(50);
  const [todaySpent, setTodaySpent] = useState(0);
  const [purchaseHistory, setPurchaseHistory] = useState([]);
  const [incomeHistory, setIncomeHistory] = useState([]);
  const [savingsGoal, setSavingsGoal] = useState(1000);
  const [autoSaveRate, setAutoSaveRate] = useState(0.2);
  const [errors, setErrors] = useState([]);
  const [achievements, setAchievements] = useState([]);
  const [monthlyReport, setMonthlyReport] = useState({});
  
  const buyThing = (itemName: string, price: number, category: string) => {
    // 超級複雜的邏輯...
    setErrors([]);
    // 檢查預算、計算金額、更新歷史、檢查成就...
    // 一大堆 setState 呼叫
    // 代碼變得超級複雜和難維護
  };
}

useReducer 則會拯救我們的程式碼:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 所有複雜邏輯都集中在這個「超級管家」裡
function moneyManagerReducer(state, action) {
  switch (action.type) {
    case 'BUY_ITEM': {
      // 檢查預算
      // 智慧計算邏輯
      // 一次處理所有更新
      return {};
    }
    
    case 'RECEIVE_INCOME': {
      const { amount, source } = action;
      const autoSaveAmount = amount * state.settings.autoSaveRate;
      const cashAmount = amount - autoSaveAmount;
      
      return {};
    }
    
    default:
      return state;
  }
}

function SmartMoneyManager() {
  const [state, dispatch] = useReducer(moneyManagerReducer, {
    wallets: { cash: 100, savings: 500, redEnvelope: 200 },
    settings: { dailyBudget: 50, autoSaveRate: 0.2, savingsGoal: 1000 },
    todaySpent: 0,
    history: { purchases: [], income: [] },
    achievements: [],
    monthlyReport: {},
    errors: []
  });
  
  // 使用變得超級簡單!
  const buyThing = (itemName: string, price: number, category: string) => {
    dispatch({ type: 'BUY_ITEM', itemName, price, category });
  };
  
  const receiveIncome = (amount: number, source: string) => {
    dispatch({ type: 'RECEIVE_INCOME', amount, source });
  };
  
  return (
    <div>
      <h3>智慧零用錢管理系統</h3>
      <p>現金{state.wallets.cash}</p>
      <p>存款{state.wallets.savings}</p>
      <p>今日花費{state.todaySpent}/{state.settings.dailyBudget}</p>
      
      {state.errors.length > 0 && (
        <div>錯誤{state.errors.join(', ')}</div>
      )}
      
      <button onClick={() => buyThing('玩具車', 30, '玩具')}>
        買玩具車 (30)
      </button>
      <button onClick={() => receiveIncome(100, '幫忙做家事')}>
        收入100元
      </button>
    </div>
  );
}

useReducer 的優點在這時候就會很明顯:

  • 邏輯集中:所有複雜的業務邏輯都在 reducer 裡
  • 狀態一致:不會出現狀態不同步的問題
  • 容易測試:可以單獨測試 reducer 的每個情境
  • 容易理解:組件只負責 UI,邏輯都在 reducer
  • 容易擴展:要加新功能就加新的 action type

什麼時候該選擇什麼?

  • 狀態簡單useState
  • 邏輯簡單且只用一次useState
  • 邏輯複雜或會重複使用useReducer

useReducer 不是為了炫技,而是為了解決真正的問題。

就像你整理房間,如果只是放幾本書,你自己來就好;但如果要整理很多玩具、衣服、書本,找個會整理的機器人幫忙會更好。

大部分情況下,useState 就很好用了。但當你的狀態管理變得複雜時,useReducer 就是你的救星。

不要為了用而用,要為了解決問題而用。

以上就是今天的分享,我們下次見 👋🏻

Built with Hugo
Theme Stack designed by Jimmy