useReducer 和 useContext 怎么用?适合什么场景?

一、useReducer 是什么

useReduceruseState 的替代方案,适用于包含多个子值、或下一个 state 依赖前一个 state 的复杂状态逻辑。它借鉴了 Redux 的 action/dispatch 模式:

const [state, dispatch] = useReducer(reducer, initialState);

基本用法

const initialState = { count: 0 };

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}`);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>计数:{state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
      <button onClick={() => dispatch({ type: 'reset' })}>重置</button>
    </div>
  );
}

惰性初始化

当初始状态需要通过计算得出时,传入第三个参数 init 函数:

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

function Counter({ initialCount }) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  // init 只在首次渲染时执行
}

二、useContext 是什么

useContext 让函数组件能直接读取 Context 的值,替代了过去繁琐的 <Context.Consumer> 模式。

基本用法

import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext('light');

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

function ThemedButton() {
  const { theme, toggleTheme } = useTheme();
  return (
    <button
      onClick={toggleTheme}
      style={{
        background: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#333' : '#fff',
      }}
    >
      当前主题:{theme}
    </button>
  );
}

对比 Consumer 写法

// 旧写法:Consumer 嵌套
function OldWay() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <LocaleContext.Consumer>
          {locale => (
            <UserContext.Consumer>
              {user => <div>{theme}-{locale}-{user.name}</div>}
            </UserContext.Consumer>
          )}
        </LocaleContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

// 新写法:useContext 平铺
function NewWay() {
  const theme = useContext(ThemeContext);
  const locale = useContext(LocaleContext);
  const user = useContext(UserContext);
  return <div>{theme}-{locale}-{user.name}</div>;
}

三、useReducer + useContext 组合:轻量级状态管理

将两者结合可以实现类似 Redux 的全局状态管理模式,无需引入第三方库:

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

const TodoContext = createContext(null);
const TodoDispatchContext = createContext(null);

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return [...state, { id: Date.now(), text: action.text, done: false }];
    case 'TOGGLE':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, done: !todo.done } : todo
      );
    case 'DELETE':
      return state.filter(todo => todo.id !== action.id);
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

function TodoProvider({ children }) {
  const [todos, dispatch] = useReducer(todoReducer, []);

  return (
    <TodoContext.Provider value={todos}>
      <TodoDispatchContext.Provider value={dispatch}>
        {children}
      </TodoDispatchContext.Provider>
    </TodoContext.Provider>
  );
}

function useTodos() {
  const context = useContext(TodoContext);
  if (context === null) throw new Error('useTodos must be within TodoProvider');
  return context;
}

function useTodoDispatch() {
  const context = useContext(TodoDispatchContext);
  if (context === null) throw new Error('useTodoDispatch must be within TodoProvider');
  return context;
}

使用时各组件各取所需:

function TodoList() {
  const todos = useTodos();
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
}

function TodoItem({ todo }) {
  const dispatch = useTodoDispatch();
  return (
    <li>
      <span
        onClick={() => dispatch({ type: 'TOGGLE', id: todo.id })}
        style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
      >
        {todo.text}
      </span>
      <button onClick={() => dispatch({ type: 'DELETE', id: todo.id })}>删除</button>
    </li>
  );
}

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

  return (
    <form onSubmit={e => {
      e.preventDefault();
      dispatch({ type: 'ADD', text });
      setText('');
    }}>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button type="submit">添加</button>
    </form>
  );
}

四、性能问题与优化

Context 的重渲染问题

Context 有一个关键特性:当 Provider 的 value 变化时,所有消费该 Context 的组件都会重渲染,无论它们是否使用了变化的那部分数据。

// 问题:只要 count 变化,useTheme 的消费者也会重渲染
function AppProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [count, setCount] = useState(0);

  return (
    <AppContext.Provider value={{ theme, setTheme, count, setCount }}>
      {children}
    </AppContext.Provider>
  );
}

优化策略一:拆分 Context

将不同更新频率的数据拆分到不同的 Context:

function AppProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [count, setCount] = useState(0);

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <CounterContext.Provider value={{ count, setCount }}>
        {children}
      </CounterContext.Provider>
    </ThemeContext.Provider>
  );
}

优化策略二:分离 state 和 dispatch

dispatch 函数是稳定的引用(useReducer 保证),把它放在单独的 Context 中可以避免只需要 dispatch 的组件被 state 变化触发重渲染:

// 上面 TodoProvider 的示例就采用了这个策略
// TodoContext 放 state(变化频繁)
// TodoDispatchContext 放 dispatch(引用稳定)

优化策略三:useMemo 缓存 value

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const value = useMemo(() => ({ theme, setTheme }), [theme]);

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

五、useReducer vs useState 怎么选

场景推荐
单一独立状态(开关、计数器)useState
多个相关联的状态字段useReducer
下一个状态依赖复杂逻辑useReducer
状态更新逻辑需要被测试useReducer(reducer 是纯函数,易测试)
需要传递更新能力给深层组件useReducer + useContext
简单的表单(几个字段)useState
复杂表单(多步骤、校验)useReducer

六、useReducer + useContext vs Redux

对比维度useReducer + useContextRedux / RTK
安装依赖需要安装
学习成本中等
DevToolsRedux DevTools
中间件thunk / saga
性能优化需要手动拆分 Contextselector 精确订阅
适用规模中小型项目中大型项目
异步处理手动在组件中处理中间件统一处理

useReducer + useContext 适合中小型应用或者局部模块的状态管理。当应用规模增大、需要中间件、DevTools 调试、或精细化的性能优化时,Redux Toolkit 是更好的选择。