React 小白到高手的分水岭:学会reducer+ Context这套 useReduce,再也不怕状态传疯了!

393 阅读5分钟

从局部到全局,React 小白如何用 useReducer + useContext + 自定义 Hook 写出可维护的状态管理方案? 前言:useState 用着用着就绷不住了 当你刚学 React,写个计数器、按钮切换,useState 就像小锤子,敲敲打打啥都能做。 但很快你会发现:

  • A 组件里存了个状态
  • B、C、D 也想用这个状态
  • 结果你只能 props 一层层传,传着传着连祖宗八代都套进去了。 这就是 局部状态useState 没问题,跨层级的全局状态useState 就够呛。 所以,React 官方给了我们两个老熟人:
  • useContext :专门干跨层级共享
  • useReducer :专门干复杂状态的纯函数式改造 这俩谁都不复杂,但一旦你把它俩配在一起,再加个自定义 Hook 包装一下,
    就有了一个好用、轻量、无依赖的小型全局状态方案。

先放结论:这套组合能干啥?

一句话:

你可以把它当作一个自带 Redux 味儿的“状态仓库”,
只不过它不需要额外装库,也没有 Redux 那么重型,配置 0 成本。

常见的用法:

  • Todo 列表
  • 登录状态
  • 主题切换(黑夜模式)
  • 购物车
  • 多页签切换状态

本质都一样:全局可共享,谁用都能改,改了就全局同步。


传统方案的痛点

说到这,很多人会问:

“那我用 useState + props 传不也行吗?”

当然也行!但场景复杂一点,你就会踩坑:

  • 跨层级麻烦:你需要从最顶层一直 props 传到孙子组件,谁改动了 props,所有子组件都得跟着渲染,浪费性能。
  • 难以维护:状态放得越分散,找 bug 越痛苦,“到底谁在管这个状态”?
  • 多人协作时容易踩脚:一坨状态散在各组件里,你根本搞不清谁才是源头。

所以:状态管理要分场景,局部状态 useState,全局状态直接上仓库,咱这套就是一个轻仓库


设计思路:为什么用 useReducer + useContext + 自定义 Hook

来,捋一下咱们今天用的组合拳:


✅ 1️⃣ useReducer:定义状态形态状态更新规则

  • 用纯函数把“状态长什么样、怎么改”写死。
  • 所有更新都要 dispatch 一个 action,要改必须遵守规矩,别人不能私自偷偷改。
  • 状态可预测、可复用、可扩展,和 Redux 的 reducer 思路一模一样。

✅ 2️⃣ useContext:把 state + dispatch 挂到全局容器

  • createContext() 创建一个仓库。
  • <Context.Provider>state 和操作方法包进去。
  • 谁需要就用 useContext 拿出来,不需要 props drilling。

✅ 3️⃣ 自定义 Hook:做个“语法糖”,把这些组合一下

  • useReducer、操作方法,放进 useTodos,方便拿。
  • 再把 useContext 封到 useTodoContext,用的时候 useTodoContext() 一行就行。

咱们举个实际案例:做个简易 Todo App

下面直接用一个大家都熟悉的「待办清单」,串起这套思路。


项目要实现啥?

  • 输入框新增 todo
  • 点击 todo 打勾(完成状态)
  • 删除 todo
  • 不同组件都能操作同一份 todo 列表

技术选型

  • 状态结构:todos 是个数组,元素长 { id, text, done }
  • 状态更新:用 reducer 定规矩
  • 全局共享:用 context
  • 封装逻辑:用自定义 Hook


实操代码


✅ 1️⃣ 写 Context

// TodoContext.js
import { createContext } from 'react';

export const TodoContext = createContext(null);

创建一个上下文,默认值先 null,反正后面 Provider 会覆盖掉。


✅ 2️⃣ 写 reducer

// reducers/todoReducer.js
export default function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        { id: Date.now(), text: action.text, done: false }
      ];
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, done: !todo.done } : todo
      );
    case 'REMOVE_TODO':
      return state.filter(todo => todo.id !== action.id);
    default:
      return state;
  }
}

思路:

  • 增加:老数组 + 新 todo
  • 切换完成:map 找到目标,done 取反
  • 删除:filter 过滤掉

✅ 3️⃣ 写 useTodos(核心状态逻辑)

// hooks/useTodos.js
import { useReducer } from 'react';
import todoReducer from '../reducers/todoReducer';

export function useTodos(initial = []) {
  const [todos, dispatch] = useReducer(todoReducer, initial);

  const addTodo = (text) => dispatch({ type: 'ADD_TODO', text });
  const toggleTodo = (id) => dispatch({ type: 'TOGGLE_TODO', id });
  const removeTodo = (id) => dispatch({ type: 'REMOVE_TODO', id });

  return { todos, addTodo, toggleTodo, removeTodo };
}

关键点:

  • todos 是纯状态
  • 操作都走封装好的 addTodotoggleTodoremoveTodo,外部不直接 dispatch,更安全

✅ 4️⃣ 写 useTodoContext(小糖)

// hooks/useTodoContext.js
import { useContext } from 'react';
import { TodoContext } from '../TodoContext';

export function useTodoContext() {
  return useContext(TodoContext);
}

就是把 useContext 调用包一下,别的组件直接用。


✅ 5️⃣ 在顶层 <App> 包 Provider

// App.jsx
import { TodoContext } from './TodoContext';
import { useTodos } from './hooks/useTodos';
import AddTodo from './components/AddTodo';
import TodoList from './components/TodoList';

export default function App() {
  const todoHook = useTodos([]);

  return (
    <TodoContext.Provider value={todoHook}>
      <h1>Todo App</h1>
      <AddTodo />
      <TodoList />
    </TodoContext.Provider>
  );
}

useTodos 返回的值挂到 Provider,所有后代组件都能用。


✅ 6️⃣ AddTodo:输入框负责新增

// components/AddTodo.js
import { useState } from 'react';
import { useTodoContext } from '../hooks/useTodoContext';

export default function AddTodo() {
  const [text, setText] = useState('');
  const { addTodo } = useTodoContext();

  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim()) {
      addTodo(text.trim());
      setText('');
    }
  };

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

✅ 7️⃣ TodoList:展示 + 操作

// components/TodoList.js
import { useTodoContext } from '../hooks/useTodoContext';

export default function TodoList() {
  const { todos, toggleTodo, removeTodo } = useTodoContext();

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <span
            onClick={() => toggleTodo(todo.id)}
            style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
          >
            {todo.text}
          </span>
          <button onClick={() => removeTodo(todo.id)}>remove</button>
        </li>
      ))}
    </ul>
  );
}

到这里,一个可维护的全局 Todo App 就写完了

  • 输入新增
  • 点击切换
  • 点击删除
  • 所有组件实时共享,自动响应式

没有多余 props,一层 Provider 管到底。



这套设计好在哪?

可读性强:状态和操作分离,所有状态改动可追溯
易扩展:想要 filter、编辑 todo,只要在 reducer 里加个 case
无外部依赖:纯 React,零学习成本
可迁移:以后真要换 Redux,reducer 这套思想一模一样,几乎能平滑迁移



思考升级:遇到别的场景怎么办?

把这个思路举一反三:

  • 购物车?就是 todos 换成 cartItems
  • 登录状态?就是 user 对象 + loginlogout 的 action
  • 主题?就一个 theme 字符串 + toggleTheme 方法

只要你能把状态和操作抽成 reducer + context,全局状态管理就稳得一批。


🎉 写在最后

useStateuseReducer + useContext,你经历的是:
“我能把局部玩转 ➜ 我能把全局玩转”
这是从「会写页面」到「懂 React 状态管理」的必经之路。

要是对你有用,麻烦一键三连,评论区甩一个:

“老哥,还想看别的 Hook 组合玩法!”

咱们下次给你安排更硬核的全局状态方案、异步请求管理、数据持久化,一套套全给你抠干净!


React,不止是写组件,更是写思想。
祝你越写越顺,越写越帅!