第 14 章:useReducer
学习目标
- 理解 useReducer 与 useState 的关系
- 掌握 reducer 函数的写法(纯函数)
- 用 useReducer + Context 封装迷你 Redux
- 避开 useReducer 的 3 个常见坑
一、useReducer 是什么?
useReducer 是 useState 的替代方案,用 reducer 函数管理更复杂的状态:
tsx
const [state, dispatch] = useReducer(reducer, initialState);state— 当前状态dispatch— 发送 action,触发 reducerreducer(state, action)— 纯函数,返回新状态
二、基础用法
tsx
interface State { count: number; }
type Action = { type: 'inc' } | { type: 'dec' } | { type: 'reset' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'inc': return { count: state.count + 1 };
case 'dec': return { count: state.count - 1 };
case 'reset': return { count: 0 };
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: 'inc' })}>+</button>
<button onClick={() => dispatch({ type: 'dec' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>重置</button>
</>
);
}三、action 带参数
tsx
type Action =
| { type: 'set'; value: number }
| { type: 'incBy'; step: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'set': return { count: action.value };
case 'incBy': return { count: state.count + action.step };
default: return state;
}
}
// 派发
dispatch({ type: 'set', value: 100 });
dispatch({ type: 'incBy', step: 5 });四、useReducer vs useState
| 维度 | useState | useReducer |
|---|---|---|
| 适合 | 简单 state | 复杂 state(多字段、关联) |
| 改值 | 直接 set | 派发 action |
| 复用 | 无 | reducer 可拆文件复用 |
| 调试 | 一般 | action 列表清晰,可加日志 |
| 测试 | 一般 | reducer 是纯函数,极易测 |
选型:
- 字段 < 3 个 → useState
- 多字段 + 多个操作 → useReducer
- 关联变更(改 a 必动 b)→ useReducer
五、惰性初始化
tsx
const [state, dispatch] = useReducer(reducer, initialArg, initFn);
// initFn(initialArg) 只在 mount 时调用
function init(initial: number): State {
return { count: initial };
}
const [state, dispatch] = useReducer(reducer, 0, init);⚠️ 坑 1:init 函数是可选第三个参数,只在 mount 时跑,不要把 reducer 调用放进去。
六、嵌套对象 State
tsx
interface State {
user: { name: string; age: number } | null;
loading: boolean;
}
type Action =
| { type: 'login'; user: User }
| { type: 'logout' }
| { type: 'updateName'; name: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'login':
return { ...state, user: action.user, loading: false };
case 'logout':
return { ...state, user: null };
case 'updateName':
if (!state.user) return state;
return { ...state, user: { ...state.user, name: action.name } };
}
}⚠️ 坑 2:reducer 必须返回新对象才触发更新,不能直接
state.user.name = ...。
七、reducer 必须是纯函数
tsx
// ❌ 不允许的副作用
function badReducer(state: State, action: Action) {
localStorage.setItem('x', '1'); // ❌ 副作用
fetch('/api'); // ❌ 异步
state.count++; // ❌ 修改原 state
}
// ✅ 纯函数
function goodReducer(state: State, action: Action): State {
return { ...state, count: state.count + 1 };
}⚠️ 坑 3:reducer 内部不能有副作用、异步、随机、时间。违反会导致行为不可预测。
八、useReducer + Context = 迷你 Redux
tsx
interface State { count: number; theme: 'light' | 'dark'; }
type Action = { type: 'inc' } | { type: 'toggleTheme' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'inc': return { ...state, count: state.count + 1 };
case 'toggleTheme': return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
}
}
const StoreContext = createContext<{
state: State;
dispatch: React.Dispatch<Action>;
} | null>(null);
function StoreProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(reducer, { count: 0, theme: 'light' });
return <StoreContext.Provider value={{ state, dispatch }}>{children}</StoreContext.Provider>;
}
function useStore() {
const ctx = useContext(StoreContext);
if (!ctx) throw new Error('missing StoreProvider');
return ctx;
}
// 任意组件使用
function Counter() {
const { state, dispatch } = useStore();
return (
<>
<p>{state.count} - {state.theme}</p>
<button onClick={() => dispatch({ type: 'inc' })}>+</button>
<button onClick={() => dispatch({ type: 'toggleTheme' })}>切换主题</button>
</>
);
}九、实战:Todo Reducer
tsx
interface Todo { id: number; text: string; done: boolean; }
interface State { list: Todo[]; }
type Action =
| { type: 'add'; text: string }
| { type: 'toggle'; id: number }
| { type: 'remove'; id: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'add':
return { list: [...state.list, { id: Date.now(), text: action.text, done: false }] };
case 'toggle':
return { list: state.list.map(t => t.id === action.id ? { ...t, done: !t.done } : t) };
case 'remove':
return { list: state.list.filter(t => t.id !== action.id) };
}
}十、本章小结
| 要点 | 关键 |
|---|---|
| 签名 | const [state, dispatch] = useReducer(reducer, initial) |
| 适用 | 多字段、多 action、关联变更 |
| reducer | 纯函数,switch 返回新 state |
| 副作用 | 不要在 reducer 里调 API/localStorage |
| 惰性初始化 | 第三个参数 init 函数 |
| 配合 Context | 全局状态管理,无依赖第三方库 |
| 测试 | reducer 是纯函数,直接 expect(reducer(...)).toEqual(...) |
动手练习
- 计算器:支持 +、-、×、÷、清空,用 useReducer
- Todo 完整版:用 useReducer 重写第 10 章的 Todo
- 全局 store:reducer + Context 封装,两个组件共享同一个状态
- 找坑:以下 reducer 哪里错?tsx
case 'inc': state.count++; return state;
下一章:第 15 章:自定义 Hook →