「前端何去何从」混乱到有序的状态管理: Reducer 与 Context

React Reducer 与 Context:从混乱到有序的状态管理

当你的 useState 开始打架,当 props 传了七八层还在继续传,说明你需要换一种方式组织状态了,原生 react 怎么去解决这种问题?

开篇:为什么需要 Reducer 和 Context

在 React 里写交互,大多数人都是从 useState 开始的。

一个输入框、一个按钮、一个开关------用 useState 完全够用。但随着功能变复杂,你会开始遇到两个很实际的问题:

问题一:状态逻辑越来越散

假设你在做一个任务列表,用户可以添加、编辑、删除任务。一开始你可能会这样写:

scss 复制代码
const [tasks, setTasks] = useState(initialTasks);

function handleAddTask(text) {
  setTasks([...tasks, { id: nextId++, text, done: false }]);
}

function handleChangeTask(task) {
  setTasks(tasks.map(t => (t.id === task.id ? task : t)));
}

function handleDeleteTask(taskId) {
  setTasks(tasks.filter(t => t.id !== taskId));
}

这能跑,但随着操作变多,逻辑会越来越分散。每个事件处理函数都在单独修改同一个 state,你很难一眼看出"某个操作到底改了什么"。

问题二:props 传了七八层

另一个常见场景:你有一个用户对象在顶层组件里,但需要在五六层深的子组件里使用。于是你不得不一层一层往下传:

xml 复制代码
<Page user={user}>
  <Layout user={user}>
    <Sidebar user={user} />
    <Content user={user}>
      <Profile user={user} />
    </Content>
  </Layout>
</Page>

中间那些组件(Layout、Sidebar、Content)根本不关心 user,它们只是被迫当"快递员"。这就是 prop drilling------逐层透传 props。

这两个问题,React 都有原生方案解决:

  • Reducer 解决"逻辑分散"
  • Context 解决"传递太深"

这篇文章就是把这两个东西讲清楚。

本文会覆盖什么

  • useReducer 核心概念和用法
  • Reducer 函数的设计原则
  • Context 的创建、提供、消费
  • Reducer + Context 组合模式
  • 什么时候该用,什么时候不该用

文章的目标不是让你记住几个 API 名字,而是建立一套稳定的状态组织思维。


Part 1: useState 的局限性

在讲 Reducer 和 Context 之前,我们先看清楚 useState 到底哪里不够用。

逻辑分散问题

useState 管理状态时,更新逻辑是写在各个事件处理函数里的。

一个简单的表单可能长这样:

scss 复制代码
const [status, setStatus] = useState('typing');
const [error, setError] = useState(null);
const [answer, setAnswer] = useState('');

async function handleSubmit(e) {
  e.preventDefault();
  setStatus('submitting');
  setError(null);
  try {
    await submitForm(answer);
    setStatus('success');
  } catch (err) {
    setError(err);
    setStatus('typing');
  }
}

这段代码不算复杂,但你已经能看出一个问题:状态更新逻辑散落在不同的地方

setStatus('submitting')setError(null)setStatus('success')setStatus('typing')------这些调用分散在 handleSubmit 里,如果你想知道"提交失败时到底改了哪些状态",你需要仔细读整个函数。

当操作变多、状态变复杂后,这个问题会越来越明显。你会开始在不同函数里反复调用 setXxx(...),然后越来越难看清"某个操作到底改了什么"。

prop drilling 问题

这是另一个经典问题。

假设你在做一个博客系统,用户信息存在 App 组件里,但需要在 Article 组件里显示作者头像。组件树长这样:

xml 复制代码
<App>
  <Layout>
    <Sidebar />
    <Main>
      <ArticleList>
        <Article />
      </ArticleList>
    </Main>
  </Layout>
</App>

如果不做任何处理,你可能需要这样传:

xml 复制代码
<App user={user}>
  <Layout user={user}>
    <Sidebar />
    <Main user={user}>
      <ArticleList user={user}>
        <Article user={user} />
      </ArticleList>
    </Main>
  </Layout>
</App>

LayoutMainArticleList 这三个组件根本不需要 user,它们只是被迫当"快递员"。

这种写法的问题不只是"代码难看",而是:

  • 中间组件和 user 产生了不必要的耦合
  • 如果以后 user 的结构变了,你需要改所有中间层的 props
  • 读代码时很难追踪数据到底从哪来

为什么需要更好的组织方式

这两个问题的根源是一样的:当应用变复杂后,useState + props 的默认方式不够用了

useState 适合管理简单的、局部的状态。但当:

  • 同一个状态会被很多事件处理函数修改
  • 状态逻辑越来越长,越来越分散
  • 多个组件需要读写同一份数据
  • 组件层级太深,props 传得太多

你就需要更结构化的方案。

React 提供了两个原生工具:

  • useReducer:把状态更新逻辑集中到一个地方
  • Context:让数据"跳过"中间层直达需要它的组件

下面我们就一个一个讲。


Part 2: Reducer 是什么

从生活例子理解 Reducer

想象你去银行办业务。

没有 Reducer 的情况:你直接走到柜台,告诉柜员"我要存 500 块"、"我要取 200 块"、"我要查余额"。每个操作都是直接的、独立的。

有 Reducer 的情况:你先填一张表,写清楚"操作类型:存款,金额:500",然后把表交给柜员。柜员根据表格内容,统一处理。

Reducer 就是那个"柜员"。它接收两个东西:

  1. 当前状态(state
  2. 一个描述"发生了什么"的 action

然后返回一个新的状态。

javascript 复制代码
function bankReducer(balance, action) {
  switch (action.type) {
    case 'deposit':
      return balance + action.amount;
    case 'withdraw':
      return balance - action.amount;
    default:
      throw Error('Unknown action: ' + action.type);
  }
}

Reducer 的三个要素

每个 Reducer 都有三个核心概念:

概念 含义 例子
state 当前状态 { balance: 1000 }
action 描述"发生了什么"的对象 { type: 'deposit', amount: 500 }
dispatch 触发状态更新的函数 dispatch({ type: 'deposit', amount: 500 })

流程是这样的:

scss 复制代码
用户操作 → dispatch(action) → Reducer(state, action) → 新的 state

纯函数原则

Reducer 有一个重要约束:它必须是纯函数

什么是纯函数?简单说就是:

  • 同样的输入,永远得到同样的输出
  • 不会修改外部变量
  • 不会发请求、不会改 DOM、不会做任何"副作用"
javascript 复制代码
// ❌ 不是纯函数:修改了外部变量
let total = 0;
function badReducer(state, action) {
  total += action.amount; // 副作用!
  return { ...state, balance: state.balance + action.amount };
}

// ✅ 纯函数:只根据输入计算输出
function goodReducer(state, action) {
  return { ...state, balance: state.balance + action.amount };
}

为什么必须是纯函数?因为 React 需要能够:

  • 随时重新执行 reducer 来计算新状态
  • 在开发模式下多次执行来检测问题
  • 在并发模式下安全地处理状态更新

如果 reducer 有副作用,这些场景都会出问题。

action 要表达业务动作

好的 action 描述的是"用户做了什么",而不是"我要调用哪个 setter"。

php 复制代码
// ❌ 不好的 action:描述的是技术操作
dispatch({ type: 'setStatus', value: 'submitting' });
dispatch({ type: 'setError', value: null });

// ✅ 好的 action:描述的是业务动作
dispatch({ type: 'submit_started' });
dispatch({ type: 'submit_failed', error: err });

好的 action 让你一看就知道"发生了什么",而不是"改了哪个变量"。


Part 3: useReducer 实战

基本语法

useReducer 的用法和 useState 类似,但返回三个值:

scss 复制代码
const [state, dispatch] = useReducer(reducer, initialState);
  • state:当前状态
  • dispatch:触发更新的函数
  • reducer:你写的处理函数
  • initialState:初始状态

来看一个完整的例子:

php 复制代码
import { useReducer } from 'react';

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added':
      return [...tasks, { id: action.id, text: action.text, done: false }];
    case 'changed':
      return tasks.map(task => (task.id === action.task.id ? action.task : task));
    case 'deleted':
      return tasks.filter(task => task.id !== action.id);
    default:
      throw Error('Unknown action: ' + action.type);
  }
}

function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({ type: 'added', id: nextId++, text });
  }

  function handleChangeTask(task) {
    dispatch({ type: 'changed', task });
  }

  function handleDeleteTask(taskId) {
    dispatch({ type: 'deleted', id: taskId });
  }

  // ...
}

从 useState 迁移到 useReducer

迁移过程分三步:

第一步:把设置 state 的逻辑改写成 dispatch action

php 复制代码
// 之前
function handleAddTask(text) {
  setTasks([...tasks, { id: nextId++, text, done: false }]);
}

// 之后
function handleAddTask(text) {
  dispatch({ type: 'added', id: nextId++, text });
}

第二步:编写 reducer 函数

把所有更新逻辑集中到一个地方:

python 复制代码
function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added':
      return [...tasks, { id: action.id, text: action.text, done: false }];
    case 'changed':
      return tasks.map(task => (task.id === action.task.id ? action.task : task));
    case 'deleted':
      return tasks.filter(task => task.id !== action.id);
    default:
      throw Error('Unknown action: ' + action.type);
  }
}

第三步:在组件中用 useReducer

scss 复制代码
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

什么时候该用 useReducer

一个简单的判断标准:

场景 推荐
只有一两个简单状态 useState
状态逻辑复杂,操作很多 useReducer
多个事件处理函数修改同一个状态 useReducer
想更容易测试状态更新逻辑 useReducer

我的经验是:只要你开始频繁在不同函数里改同一份 state,就该警觉了。因为这往往意味着后面会越来越难维护。

不要为了"高级"而用 reducer。它的价值在于整理复杂逻辑,不在于替代所有 useState

常见错误:reducer 里做副作用

php 复制代码
// ❌ 错误:reducer 里发请求
function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added':
      const response = await fetch('/api/tasks', { method: 'POST', body: action.text });
      return [...tasks, response.json()];
    // ...
  }
}

// ✅ 正确:在事件处理函数或 Effect 里发请求
async function handleAddTask(text) {
  const response = await fetch('/api/tasks', { method: 'POST', body: text });
  const newTask = await response.json();
  dispatch({ type: 'added', task: newTask });
}

Reducer 只负责计算新状态,副作用应该在外面做。


Part 4: Context 解决什么问题

prop drilling 的痛点

前面说过,prop drilling 会让中间组件被迫当"快递员"。

来看一个更具体的例子。假设你在做一个主题切换功能:

javascript 复制代码
function App() {
  const [theme, setTheme] = useState('light');

  return (
    <Layout theme={theme}>
      <Header theme={theme} />
      <Main theme={theme}>
        <Sidebar theme={theme} />
        <Content theme={theme}>
          <Button theme={theme} />
        </Content>
      </Main>
    </Layout>
  );
}

LayoutHeaderMainSidebarContent 都不需要 theme,它们只是被迫传下去。如果以后要加一个 setTheme,又得多传一层。

Context 的三步曲

Context 提供了一种"跨中间层传值"的机制。分三步:

第一步:创建 context

javascript 复制代码
import { createContext } from 'react';

export const ThemeContext = createContext('light');

createContext 的参数是默认值,当上层没有 provider 时使用。

第二步:提供 context

javascript 复制代码
function App() {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext value={theme}>
      <Layout />
    </ThemeContext>
  );
}

ThemeContextvalue 属性就是你要传递的值。

第三步:消费 context

javascript 复制代码
import { useContext } from 'react';

function Button() {
  const theme = useContext(ThemeContext);

  return (
    <button className={theme === 'dark' ? 'btn-dark' : 'btn-light'}>
      点击我
    </button>
  );
}

useContext(SomeContext) 会读取最近的 provider 提供的值。如果上层没有 provider,就使用 createContext 时传入的默认值。

对比一下

xml 复制代码
// ❌ 没有 Context:层层传递
<Layout theme={theme}>
  <Header theme={theme} />
  <Main theme={theme}>
    <Button theme={theme} />
  </Main>
</Layout>

// ✅ 有 Context:直达目标
<ThemeContext value={theme}>
  <Layout>
    <Header />
    <Main>
      <Button />
    </Main>
  </Layout>
</ThemeContext>

中间组件不再需要关心 theme,它们变干净了。

什么时候该用 Context

Context 适合传递"周围环境"类的数据:

  • 主题(theme)
  • 当前用户(currentUser)
  • 语言环境(locale)
  • 路由信息(router)
  • 某个功能域内多层组件都要读取的状态

什么时候不该急着用 Context

如果只是传一两层,props 往往更直接。

因为 props 的依赖关系是显式的,读代码时很容易看懂。而 Context 一旦用多了,数据来源会变得不够直观。

先问自己:这个值是真的"很多层都要用"吗?如果不是,就先用 props。


Part 5: Reducer + Context 组合模式

到这里,你已经学了两件事:

  • useReducer 可以整理复杂的状态更新逻辑
  • Context 可以避免层层传递 props

把它们结合起来,就是 React 原生组织复杂状态的一种常见方式。

典型问题

假设你在做一个 TodoApp,状态在顶层:

scss 复制代码
const [todos, dispatch] = useReducer(todosReducer, initialTodos);

接下来你会发现:

  • TodoList 需要读 todos
  • AddTodo 需要用 dispatch
  • TodoItem 也需要用 dispatch

如果继续用 props 层层往下传,组件树很快会变得臃肿。

分离 state 和 dispatch

常见做法是把 statedispatch 放进两个不同的 context:

javascript 复制代码
import { createContext } from 'react';

export const TodosContext = createContext(null);
export const TodosDispatchContext = createContext(null);

然后统一提供:

javascript 复制代码
function TodoApp() {
  const [todos, dispatch] = useReducer(todosReducer, initialTodos);

  return (
    <TodosContext value={todos}>
      <TodosDispatchContext value={dispatch}>
        <h1>待办事项</h1>
        <AddTodo />
        <TodoList />
      </TodosDispatchContext>
    </TodosContext>
  );
}

深层组件直接读取:

javascript 复制代码
function TodoList() {
  const todos = useContext(TodosContext);

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

function AddTodo() {
  const dispatch = useContext(TodosDispatchContext);

  function handleAdd(text) {
    dispatch({ type: 'added', id: nextId++, text });
  }

  // ...
}

为什么分成两个 context

把读取状态和触发更新分开,有一个好处:

读代码时你会更容易分清:哪些组件只是消费数据,哪些组件还会发起更新。

scss 复制代码
// 只读数据
function TodoList() {
  const todos = useContext(TodosContext);
  // ...
}

// 只触发更新
function AddTodo() {
  const dispatch = useContext(TodosDispatchContext);
  // ...
}

自定义 Hook 封装

为了让业务组件更干净,可以把 context 的读取封装成自定义 Hook:

scss 复制代码
function useTodos() {
  return useContext(TodosContext);
}

function useTodosDispatch() {
  return useContext(TodosDispatchContext);
}

这样业务组件用起来更简洁:

javascript 复制代码
function TodoList() {
  const todos = useTodos();

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

而且如果以后你想换实现方式(比如换成 Zustand),只需要改 Hook 内部,业务组件不用动。

完整的 TodoApp 案例

把上面的代码整合起来:

javascript 复制代码
import { createContext, useContext, useReducer } from 'react';

// Context
const TodosContext = createContext(null);
const TodosDispatchContext = createContext(null);

// Reducer
function todosReducer(todos, action) {
  switch (action.type) {
    case 'added':
      return [...todos, { id: action.id, text: action.text, done: false }];
    case 'changed':
      return todos.map(todo => (todo.id === action.todo.id ? action.todo : todo));
    case 'deleted':
      return todos.filter(todo => todo.id !== action.id);
    default:
      throw Error('Unknown action: ' + action.type);
  }
}

// Provider 组件
function TodosProvider({ children }) {
  const [todos, dispatch] = useReducer(todosReducer, initialTodos);

  return (
    <TodosContext value={todos}>
      <TodosDispatchContext value={dispatch}>
        {children}
      </TodosDispatchContext>
    </TodosContext>
  );
}

// 自定义 Hook
function useTodos() {
  return useContext(TodosContext);
}

function useTodosDispatch() {
  return useContext(TodosDispatchContext);
}

// 业务组件
function TodoList() {
  const todos = useTodos();

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

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

  function handleSubmit(e) {
    e.preventDefault();
    if (text.trim()) {
      dispatch({ type: 'added', id: nextId++, text });
      setText('');
    }
  }

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

// App
export default function App() {
  return (
    <TodosProvider>
      <h1>待办事项</h1>
      <AddTodo />
      <TodoList />
    </TodosProvider>
  );
}

这个模式把:

  • 状态定义(reducer)
  • 状态提供(context)
  • 状态消费(useContext)

分开了。每个部分职责清晰,也更容易测试和维护。


Part 6: 什么时候不该用

简单组件不需要

如果你的组件只有一两个简单状态,useState 完全够用。

scss 复制代码
// 这种情况不需要 useReducer
const [isOpen, setIsOpen] = useState(false);
const [count, setCount] = useState(0);

Reducer 的价值在于整理复杂逻辑。如果你的逻辑不复杂,用 reducer 反而增加了代码量。

和外部状态管理库的对比

当应用规模变大后,你可能会考虑引入外部状态管理库(如 Zustand、Redux、Jotai 等)。

方案 适合场景 优点 缺点
useState 简单、局部状态 零配置,最简单 逻辑分散后难维护
useReducer 单组件复杂状态 逻辑集中,纯函数 只在单组件内有效
Reducer + Context 跨组件共享状态 原生方案,零依赖 性能可能有问题
Zustand / Jotai 中大型应用 简洁,性能好 需要引入依赖
Redux 大型团队协作 生态完善,可预测 样板代码多,学习曲线陡

个人建议:先用 Reducer + Context,等你真正感觉到它的局限(比如性能问题、代码量问题),再考虑外部库。不要过早优化。

选型建议

简单判断:

  • 只是局部状态useState
  • 单组件复杂逻辑useReducer
  • 跨组件共享,规模不大:Reducer + Context
  • 跨组件共享,规模较大:考虑 Zustand / Jotai
  • 大型团队,需要严格规范:考虑 Redux

实际项目中,大多数情况用 useState + props 就够了。当你真正需要时,Reducer + Context 是一个很好的起点。


总结

这篇文章讲了两个核心概念:

  • Reducer:把状态更新逻辑集中到一个纯函数里,让"发生了什么"和"怎么更新"分开
  • Context:让数据"跳过"中间层直达需要它的组件,避免 prop drilling

把它们组合起来,就是 React 原生组织复杂状态的一种常见模式:

  1. useReducer 管理状态和更新逻辑
  2. createContext 创建 context
  3. 用 Provider 把 state 和 dispatch 传下去
  4. useContext 在深层组件里读取
  5. 用自定义 Hook 封装读取逻辑

但记住:这些工具是为复杂场景准备的 。如果你的组件很简单,useState + props 就是最好的选择。


相关资源


本文基于 React 官方文档"状态管理"章节整理。

相关推荐
whuhewei5 小时前
React diff算法为什么是DFS,不是BFS
算法·react.js·深度优先
名字都不重要何况昵称6 小时前
Color Pick 2D(多 Canvas 像素拾取)
前端·canvas
BY组态6 小时前
Ricon组态系统技术深度解析:打造高性能Web可视化平台
前端·物联网·iot·web组态·组态
山屿落星辰6 小时前
Flutter 高级特性实战:动画、自定义绘制、平台通道与 Web 优化
前端·flutter
@菜菜_达7 小时前
jquery.inputmask插件介绍
前端·javascript·jquery
QuZhengRong7 小时前
【Luck-Report】缓存
java·前端·后端·vue·excel
jiayong237 小时前
前端面试题库 - 浏览器与网络篇
前端·网络·面试
Csvn7 小时前
小程序开发:微信小程序与 uni-app 实战指南
前端
摸鱼小李上线了8 小时前
vue项目页面添加水印实现方法
前端·javascript·vue.js