簡單介紹useReducer。
當 state 較為複雜時可以透過 useReducer 來把更新狀態的邏輯一個個拆開,方便管理。
const [state, dispatch] = useReducer(reducer, initialArg, init?)
reducer: reducer 是一個 function,這個 function 可以接收到兩個東西一個是當下最新的 state 跟透過 dispatch function 傳進去的參數。initialArg: 初始資料,如果沒有 init 這個參數的話會被直接放到 state。init?: 這是一個 function 可以接到 initialArg 如果初始資料需要經過計算或是處理的話就會使用到這個 function,initialArg 會被放到 init function 裡,經過處理後才會變成 state。
state: 當前最新的 state,跟 useState 的回傳值相同。dispatch: 用來啟用 reducer 來修改 state 並觸發 re-render。通常 dispatch 會接收一個物件,通常這個物件會有一個 type 屬性,用來定義這一次的 dispatch 觸發什麼事件。
useReducer hook 只能在元件裡使用,且只能在元件的最外層使用,不能放在迴圈或是判斷式裡,如果有需要可以建立一個新的子元件,放在子元件裡。
嚴格模式開啟時,在開發模式下,react 會在畫面第一次 render 時下快速的進行 mount -> unmount -> mount 的動作確保沒有多餘的 side effect 而發生錯誤,這個動作不應該對發生任何預期外的錯誤。
簡單的看過 useReducer 裡面的每一個參數跟 return 的 value 之後就來看一下範例吧。
來簡單的做一個計數器。
// reducer 有哪一些 action
const enum ActionType {
INCREASE = "INCREASE",
DECREASE = "DECREASE",
}
// dispatch 接收哪些參數
type CountAction = {
type: ActionType;
payload?: number;
};
// state
type CountObj = {
value: number;
};
function countReducer(state: CountObj, action: CountAction): CountObj {
switch (action.type) {
case ActionType.INCREASE: {
return { value: state.value + 1 };
}
case ActionType.DECREASE: {
return { value: state.value - 1 };
}
default:
return state;
}
}
這邊我定義了一個 countReducer,上面有提到 reducer 會接收兩個參數 state 是當前最新的 value,action 則是透過 dispatch 傳進來的參數,這個參數在慣例上會有 type 屬性,用來判斷要做什麼動作。
為了方便閱讀慣例上也會使用 switch 判斷式來對 type 做判斷,像上面這樣,每一個 action 應該回傳一個新的物件來改變 state,跟 useState 一樣 react 會使用 Object.is() 來做比較,所以不要使用 mutable 的方式修改 state,這樣不會觸發 re-render。
完整的 code 如下。
import { useReducer } from "react";
// reducer 有哪一些 action
const enum ActionType {
INCREASE = "INCREASE",
DECREASE = "DECREASE",
}
// dispatch 接收哪些參數
type CountAction = {
type: ActionType;
payload?: number;
};
// state
type CountObj = {
value: number;
};
const initNumber: CountObj = {
value: 0,
};
// reducer
function countReducer(state: CountObj, action: CountAction): CountObj {
switch (action.type) {
case ActionType.INCREASE: {
return { value: state.value + 1 };
}
case ActionType.DECREASE: {
return { value: state.value - 1 };
}
default:
return state;
}
}
function App() {
const [count, dispatch] = useReducer(countReducer, initNumber);
function handleIncrease() {
dispatch({ type: ActionType.INCREASE });
}
function handleDecrease() {
dispatch({ type: ActionType.DECREASE });
}
return (
<div>
<h1>Count:{count.value}</h1>
<button onClick={handleIncrease}>increase</button>
<button onClick={handleDecrease}>decrease</button>
</div>
);
}

或許會覺得這樣的功能用 useState 來實現可能還比較快,比較容易閱讀,但是如果有一個比較複雜的狀態出現時就不同了。
假設我開了一間咖啡廳,營業中會有下面這些情形。
會有下面這些行為。
const enum ActionType {
SELL_COFFEE = "SELL_COFFEE", // 賣咖啡
SELL_COFFEE_BY_NUM = "SELL_COFFEE_BY_NUM", // 賣 N 杯咖啡
MAKE_COFFEE = "MAKE_COFFEE", // 煮咖啡
MAKE_COFFEE_BY_NUM = "MAKE_COFFEE_BY_NUM", // 煮 N 杯咖啡
REPLENISHMENT = "REPLENISHMENT", // 補豆子
}
reducer 這裡包含了上面所有的邏輯。
const initialState = {
coffeeBeans: 10, // 咖啡豆 10 包
coffee: 3, // 咖啡 3 杯
revenue: 1000, // 營業資金 1000 元
};
const reducer = (state: State, action: ReducerAction) => {
switch (action.type) {
case ActionType.SELL_COFFEE: // 賣咖啡
return {
...state,
coffee: state.coffee - 1, // 賣掉 1 杯咖啡
revenue: state.revenue + 80, // 1 杯 80 元
};
case ActionType.SELL_COFFEE_BY_NUM: // 賣 N 杯咖啡
return {
...state,
coffee: state.coffee - (action.num || 0), // 減掉賣出數量
revenue: state.revenue + (action.num || 0) * 80, // 賣出數量 * 80 元
};
case ActionType.MAKE_COFFEE: // 煮咖啡
return {
...state,
coffeeBeans: state.coffeeBeans - 2, // 消耗 2 包豆子
coffee: state.coffee + 1, // 增加 1 杯咖啡
};
case ActionType.MAKE_COFFEE_BY_NUM: // 煮 N 杯咖啡
return {
...state,
coffeeBeans: state.coffeeBeans - 2 * (action.num || 0), // 消耗 N * num 包豆子
coffee: state.coffee + 1 * (action.num || 0), // 增加 N 杯咖啡
};
case ActionType.REPLENISHMENT: // 補充咖啡豆
return {
...state,
coffeeBeans: state.coffeeBeans + 10, // 進貨 10 包咖啡豆
revenue: state.revenue - 200, // 花費 200 元
};
default:
return state;
}
};
而元件本身長這樣。
function App() {
const [number, setNumber] = useState<number>(1);
const [state, dispatch] = useReducer(reducer, initialState);
function handleCoffeeNumber({ target }: ChangeEvent<HTMLInputElement>) {
setNumber(Number(target.value));
}
function handleSellCoffee() {
// 當賣咖啡的時候已經沒有咖啡了的話,就先煮 5 杯咖啡再賣。
if (state.coffee === 0) {
dispatch({ type: ActionType.MAKE_COFFEE_BY_NUM, num: 5 });
}
dispatch({ type: ActionType.SELL_COFFEE });
}
function handleSellCoffeeByNum() {
dispatch({ type: ActionType.SELL_COFFEE_BY_NUM, num: number });
}
function handleMakeCoffee() {
dispatch({ type: ActionType.MAKE_COFFEE });
}
function handleReplenishment() {
dispatch({ type: ActionType.REPLENISHMENT });
}
return (
<div>
<h1>useReducer</h1>
<div>
<p>咖啡豆: {state.coffeeBeans} 包</p>
<p>咖啡: {state.coffee} 杯</p>
<p>營業額: {state.revenue} 元</p>
<label>
預定數量:
<input value={number} onChange={handleCoffeeNumber} />
</label>
</div>
<div>
<button onClick={handleSellCoffee}>賣咖啡</button>
<button onClick={handleSellCoffeeByNum}>賣 N 杯</button>
<button onClick={handleMakeCoffee}>煮咖啡</button>
<button onClick={handleReplenishment}>補咖啡豆</button>
</div>
</div>
);
}
如果要把上面 reducer 的內容全部放到元件裡面的話,元件裡面會變的非常冗長,不好閱讀跟維護。

useReducer 讓我們可以把更新狀態的邏輯跟 function handle 拆開,透過 dispatch 的方式來觸發狀態的更新,讓狀態的維護跟修改可以更清楚。
useReducer - react document
下一篇會簡單介紹 useLayoutEffect。
如果內容有誤再麻煩大家指教,我會盡快修改。
這個系列的文章會同步更新在我個人的 Medium,歡迎大家來看看 👋👋👋
Medium