从局部到全局,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是纯状态- 操作都走封装好的
addTodo、toggleTodo、removeTodo,外部不直接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对象 +login、logout的 action - 主题?就一个
theme字符串 +toggleTheme方法
只要你能把状态和操作抽成 reducer + context,全局状态管理就稳得一批。
🎉 写在最后
从 useState 到 useReducer + useContext,你经历的是:
“我能把局部玩转 ➜ 我能把全局玩转”
这是从「会写页面」到「懂 React 状态管理」的必经之路。
要是对你有用,麻烦一键三连,评论区甩一个:
“老哥,还想看别的 Hook 组合玩法!”
咱们下次给你安排更硬核的全局状态方案、异步请求管理、数据持久化,一套套全给你抠干净!
React,不止是写组件,更是写思想。
祝你越写越顺,越写越帅!