REACT ADVANCED · 139
useReducer / State Machine:當 state transition 開始變複雜
當一個 screen 有很多互相影響的 state setters,真正問題常不是「缺另一個 Hook」,而是 transition rules 已經值得集中成 reducer。
Learning outcomes
- 能區分 state shape 與 action。
- 能把 transition 寫成 pure reducer。
- 能避免互相矛盾的 boolean state。
- 能為 reducer 建立 table-driven tests。
1. Boolean 爆炸
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);這種寫法很容易產生 loading=true、success=true、error!=null 同時存在的 impossible state。
2. 用明確 state machine
const initialState = {
status: "idle",
courses: [],
error: null
};
function reducer(state, action) {
switch (action.type) {
case "load-started":
return {
...state,
status: "loading",
error: null
};
case "load-succeeded":
return {
status: "success",
courses: action.courses,
error: null
};
case "load-failed":
return {
...state,
status: "error",
error: action.message
};
default:
return state;
}
}3. Reducer 是 pure transition function
old state+action→ reducernew state
Reducer 裡不應直接 fetch、改 DOM、寫 localStorage;side effect 放在外面。
4. useReducer
const [state, dispatch] =
useReducer(
reducer,
initialState
);
dispatch({
type: "load-started"
});Project checkpoint:Course Dashboard state machine
async function loadCourses() {
dispatch({
type: "load-started"
});
try {
const courses =
await api.loadCourses();
dispatch({
type: "load-succeeded",
courses
});
} catch (error) {
dispatch({
type: "load-failed",
message:
error instanceof Error
? error.message
: "Unknown error"
});
}
}5. Testing reducer
const next = reducer(
initialState,
{
type: "load-succeeded",
courses: [course]
}
);
// assert next.status === "success"因為 reducer 是 pure function,非常適合 deterministic unit test。
Knowledge check
- 什麼時候 useState 已足夠,什麼時候 reducer 更清楚?
- 為什麼 reducer 裡不該直接 fetch?
- 把 add/remove/update 三個 transition 寫成 actions。
- 列出兩個原本 boolean 模型可能出現的 impossible states。