Hook React 18+

useReducer: 管理复杂状态

useReducer 是 useState 的替代方案,适用于复杂的状态逻辑

什么是 useReducer?

useReducer 让你使用 reducer 函数管理组件状态。它类似于 Redux 的 reducer 模式,通过 action 来描述状态变化。

TypeScript
import { useReducer } from 'react';

const [state, dispatch] = useReducer(reducer, initialArg, init?);

基本概念

  • state: 当前的状态
  • dispatch: 触发状态更新的函数
  • reducer: 根据旧状态和 action 计算新状态的函数
  • action: 描述状态变化的对象(通常有 type 属性)
何时使用 useReducer?
  • 状态逻辑复杂,包含多个子值
  • 下一个 state 依赖于之前的 state
  • 状态更新涉及多个子组件
  • 需要更可预测的状态管理

基本用法

Reducer 函数

TypeScript
// reducer 函数签名
function reducer(state, action) {
  // 根据 action.type 返回新的 state
  switch (action.type) {
    case 'ACTION_TYPE':
      return { ...state, /* 新的 state */ };
    default:
      return state;
  }
}

简单计数器示例

TypeScript
import { useReducer } from 'react';

// 1. 定义初始状态
const initialState = { count: 0 };

// 2. 定义 reducer
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return initialState;
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

// 3. 使用 useReducer
function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>计数: {state.count}</p>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'reset' })}>重置</button>
    </div>
  );
}
Reducer 必须是纯函数
  • 相同输入必定产生相同输出
  • 不能有副作用
  • 不能修改参数(state),必须返回新对象

useReducer vs useState

对比示例:计数器

TypeScript
// 使用 useState
function Counter({ initialCount = 0 }) {
  const [count, setCount] = useState(initialCount);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>加</button>
      <button onClick={() => setCount(initialCount)}>重置</button>
    </div>
  );
}

// 使用 useReducer
const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'reset':
      return { count: action.payload };
    default:
      return state;
  }
}

function Counter({ initialCount = 0 }) {
  const [state, dispatch] = useReducer(reducer, { count: initialCount });

  return (
    <div>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>加</button>
      <button onClick={() => dispatch({ type: 'reset', payload: initialCount })}>
        重置
      </button>
    </div>
  );
}
特性useStateuseReducer
适用场景简单状态复杂状态逻辑
状态更新直接设置值通过 dispatch action
更新逻辑分散在组件中集中在 reducer 中
可测试性较难测试易于测试(纯函数)
代码量较少较多
选择建议
  • 状态是简单对象/基本类型 → useState
  • 状态更新逻辑复杂 → useReducer
  • 多个子值需要一起更新 → useReducer
  • 下一个 state 依赖于前一个 state → useReducer

复杂示例:Todo List

TypeScript
import { useReducer, useState } from 'react';

// 1. 定义初始状态
const initialState = {
  todos: [],
  filter: 'all', // 'all', 'active', 'completed'
};

// 2. 定义 action 类型
const ACTIONS = {
  ADD_TODO: 'add_todo',
  TOGGLE_TODO: 'toggle_todo',
  DELETE_TODO: 'delete_todo',
  SET_FILTER: 'set_filter',
  EDIT_TODO: 'edit_todo',
};

// 3. 定义 reducer
function todosReducer(state, action) {
  switch (action.type) {
    case ACTIONS.ADD_TODO: {
      const newTodo = {
        id: Date.now(),
        text: action.payload.text,
        completed: false,
      };
      return {
        ...state,
        todos: [...state.todos, newTodo],
      };
    }

    case ACTIONS.TOGGLE_TODO: {
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload.id
            ? { ...todo, completed: !todo.completed }
            : todo
        ),
      };
    }

    case ACTIONS.DELETE_TODO: {
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload.id),
      };
    }

    case ACTIONS.EDIT_TODO: {
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload.id
            ? { ...todo, text: action.payload.text }
            : todo
        ),
      };
    }

    case ACTIONS.SET_FILTER: {
      return {
        ...state,
        filter: action.payload.filter,
      };
    }

    default:
      return state;
  }
}

// 4. 使用 useReducer
function TodoApp() {
  const [state, dispatch] = useReducer(todosReducer, initialState);
  const [inputValue, setInputValue] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (inputValue.trim()) {
      dispatch({ type: ACTIONS.ADD_TODO, payload: { text: inputValue } });
      setInputValue('');
    }
  };

  const getFilteredTodos = () => {
    switch (state.filter) {
      case 'active':
        return state.todos.filter(todo => !todo.completed);
      case 'completed':
        return state.todos.filter(todo => todo.completed);
      default:
        return state.todos;
    }
  };

  const filteredTodos = getFilteredTodos();

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="添加新任务..."
        />
        <button type="submit">添加</button>
      </form>

      <div>
        <button onClick={() => dispatch({ type: ACTIONS.SET_FILTER, payload: { filter: 'all' } })}>
          全部
        </button>
        <button onClick={() => dispatch({ type: ACTIONS.SET_FILTER, payload: { filter: 'active' } })}>
          进行中
        </button>
        <button onClick={() => dispatch({ type: ACTIONS.SET_FILTER, payload: { filter: 'completed' } })}>
          已完成
        </button>
      </div>

      <ul>
        {filteredTodos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => dispatch({ type: ACTIONS.TOGGLE_TODO, payload: { id: todo.id } })}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
            <button onClick={() => dispatch({ type: ACTIONS.DELETE_TODO, payload: { id: todo.id } })}>
              删除
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

惰性初始化

useReducer 支持惰性初始化,可以选择性地传递第三个参数 init 函数。

TypeScript
// 初始化函数
function init(initialState) {
  return {
    ...initialState,
    // 计算初始值
    computed: expensiveComputation(),
  };
}

// 使用
function Component({ propValue }) {
  const [state, dispatch] = useReducer(
    reducer,
    { value: propValue },
    init // init 函数只在初始渲染时调用
  );

  // ...
}

示例:从 props 初始化状态

TypeScript
function init(initialCount) {
  return { count: initialCount };
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return init(action.payload);
    default:
      return state;
  }
}

function Counter({ initialCount = 0 }) {
  const [state, dispatch] = useReducer(
    reducer,
    initialCount,
    init
  );

  return (
    <div>
      <p>计数: {state.count}</p>
      <button onClick={() => dispatch({ type: 'reset', payload: initialCount })}>
        重置
      </button>
    </div>
  );
}
惰性初始化的好处
  • 避免每次渲染都重新计算初始值
  • 适用于计算量大的初始化逻辑
  • init 函数只在初始渲染时调用一次

结合 Context 使用

useReducer 与 Context 结合使用,可以实现类似 Redux 的全局状态管理。

创建 Context

TypeScript
import { createContext, useContext, useReducer } from 'react';

// 1. 创建 Context
const TodosContext = createContext(null);

// 2. 定义 reducer
function todosReducer(state, action) {
  switch (action.type) {
    case 'add':
      return [...state, { id: Date.now(), text: action.payload, completed: false }];
    case 'toggle':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    case 'delete':
      return state.filter(todo => todo.id !== action.id);
    default:
      return state;
  }
}

// 3. 创建 Provider
function TodosProvider({ children }) {
  const [state, dispatch] = useReducer(todosReducer, []);

  return (
    <TodosContext.Provider value={{ state, dispatch }}>
      {children}
    </TodosContext.Provider>
  );
}

// 4. 创建自定义 Hook
function useTodos() {
  const context = useContext(TodosContext);
  if (!context) {
    throw new Error('useTodos must be used within TodosProvider');
  }
  return context;
}

// 5. 使用
function TodoList() {
  const { state: todos, dispatch } = useTodos();

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => dispatch({ type: 'toggle', id: todo.id })}
          />
          {todo.text}
          <button onClick={() => dispatch({ type: 'delete', id: todo.id })}>
            删除
          </button>
        </li>
      ))}
    </ul>
  );
}

function AddTodo() {
  const { dispatch } = useTodos();
  const [text, setText] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim()) {
      dispatch({ type: 'add', payload: text });
      setText('');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button type="submit">添加</button>
    </form>
  );
}

// 根组件
function App() {
  return (
    <TodosProvider>
      <AddTodo />
      <TodoList />
    </TodosProvider>
  );
}
模式说明

这是实现全局状态管理的常用模式:

  1. 创建 Context 存储 state 和 dispatch
  2. 创建 Provider 包裹应用
  3. 创建自定义 Hook 简化使用
  4. 在组件中使用 Hook 访问和更新状态

最佳实践

1. 使用常量定义 action 类型

TypeScript
// ✅ 好:使用常量避免拼写错误
const ActionTypes = {
  INCREMENT: 'INCREMENT',
  DECREMENT: 'DECREMENT',
  RESET: 'RESET',
};

dispatch({ type: ActionTypes.INCREMENT });

// ❌ 差:容易拼写错误
dispatch({ type: 'INCRMENT' }); // 拼写错误,难以发现

2. 使用 action creators

TypeScript
// ✅ 好:使用 action creators 封装 action 创建
const actionCreators = {
  addTodo: (text) => ({ type: 'add_todo', payload: { text, id: Date.now() } }),
  toggleTodo: (id) => ({ type: 'toggle_todo', payload: { id } }),
  deleteTodo: (id) => ({ type: 'delete_todo', payload: { id } }),
};

// 使用
dispatch(actionCreators.addTodo('学习 React'));

// ❌ 差:手动创建 action
dispatch({ type: 'add_todo', payload: { text: '学习 React', id: Date.now() } });

3. 保持 reducer 纯净

TypeScript
// ✅ 好:纯函数 reducer
function reducer(state, action) {
  switch (action.type) {
    case 'fetch_success':
      return { ...state, data: action.payload, loading: false };
    default:
      return state;
  }
}

// ❌ 差:副作用
function reducer(state, action) {
  switch (action.type) {
    case 'fetch_data':
      fetch('/api/data').then(data => {
        // 不要在 reducer 中执行副作用!
        dispatch({ type: 'fetch_success', payload: data });
      });
      return state;
    default:
      return state;
  }
}

// ✅ 正确:副作用放在 useEffect 中
useEffect(() => {
  let cancelled = false;
  fetchData().then(data => {
    if (!cancelled) {
      dispatch({ type: 'fetch_success', payload: data });
    }
  });
  return () => { cancelled = true; };
}, []);

4. 使用 TypeScript 类型

TypeScript
// 定义 State 和 Action 类型
type State = {
  count: number;
};

type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset'; payload: number };

// 使用类型
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: action.payload };
    default:
      return state;
  }
}

5. 拆分大型 reducer

TypeScript
// ✅ 好:拆分为多个小 reducer
function userReducer(state, action) {
  switch (action.type) {
    case 'set_user':
      return { ...state, user: action.payload };
    default:
      return state;
  }
}

function todosReducer(state, action) {
  switch (action.type) {
    case 'add_todo':
      return { ...state, todos: [...state.todos, action.payload] };
    default:
      return state;
  }
}

// 使用 combineReducers 组合
function rootReducer(state, action) {
  return {
    user: userReducer(state.user, action),
    todos: todosReducer(state.todos, action),
  };
}

常见错误

1. 直接修改 state

TypeScript
// ❌ 错误: 直接修改 state
function reducer(state, action) {
  state.todos.push(action.payload); // 直接修改!
  return state;
}

// ✅ 正确: 返回新对象
function reducer(state, action) {
  return {
    ...state,
    todos: [...state.todos, action.payload],
  };
}

2. 在 reducer 中执行副作用

TypeScript
// ❌ 错误: 在 reducer 中调用 API
function reducer(state, action) {
  if (action.type === 'fetch') {
    fetch('/api/data').then(res => res.json()); // 副作用!
  }
  return state;
}

// ✅ 正确: 在 useEffect 中调用
useEffect(() => {
  const fetchData = async () => {
    const data = await fetch('/api/data').then(res => res.json());
    dispatch({ type: 'fetch_success', payload: data });
  };
  fetchData();
}, []);

3. 忘记返回 state

TypeScript
// ❌ 错误: 忘记返回 state
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    // 忘记 default!
  }
}

// ✅ 正确: 总是返回 state
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    default:
      return state; // 必须有 default
  }
}

性能优化

使用 useMemo 缓存计算

TypeScript
import { useMemo } from 'react';

function TodoList() {
  const [state, dispatch] = useReducer(todosReducer, initialState);

  // 缓存过滤后的 todos,避免每次渲染都重新计算
  const filteredTodos = useMemo(() => {
    return state.todos.filter(todo => {
      if (state.filter === 'active') return !todo.completed;
      if (state.filter === 'completed') return todo.completed;
      return true;
    });
  }, [state.todos, state.filter]);

  return <ul>{filteredTodos.map(todo => <TodoItem key={todo.id} todo={todo} />)}</ul>;
}

使用 useCallback 稳定 dispatch

TypeScript
// dispatch 引用是稳定的,不需要 useCallback
function TodoItem({ todo }) {
  const handleToggle = () => {
    dispatch({ type: 'toggle', id: todo.id }); // dispatch 稳定
  };

  return <button onClick={handleToggle}>{todo.text}</button>;
}

下一步

现在你已经了解了 useReducer,可以继续学习:

这篇文章有帮助吗?