Skip to content
第 14 章 前端 ⏱ 12 分钟阅读

第 14 章:useReducer ​

学习目标 ​

  • 理解 useReducer 与 useState 的关系
  • 掌握 reducer 函数的写法(纯函数)
  • 用 useReducer + Context 封装迷你 Redux
  • 避开 useReducer 的 3 个常见坑

一、useReducer 是什么? ​

useReducer 是 useState 的替代方案,用 reducer 函数管理更复杂的状态:

tsx
const [state, dispatch] = useReducer(reducer, initialState);
  • state — 当前状态
  • dispatch — 发送 action,触发 reducer
  • reducer(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 ​

维度useStateuseReducer
适合简单 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(...)

动手练习 ​

  1. 计算器:支持 +、-、×、÷、清空,用 useReducer
  2. Todo 完整版:用 useReducer 重写第 10 章的 Todo
  3. 全局 store:reducer + Context 封装,两个组件共享同一个状态
  4. 找坑:以下 reducer 哪里错?
    tsx
    case 'inc': state.count++; return state;

下一章:第 15 章:自定义 Hook →

本站基于 VitePress 构建 · 由 StackHub 团队维护