写 React 应用时,你是不是经常纠结:状态该放在哪个组件?深层子组件需要用某个状态,难道要一层一层传 props?遇到复杂状态(比如 Todo 列表的增删改查),用 useState 感觉力不从心,又不想引入 Redux 这种 "重型武器"------ 别愁了,用useReducer + Context
组合就能优雅解决这些问题!
一、项目架构设计:为什么选择 useReducer+Context?
(1)传统方案的痛点
- 状态提升:把状态放在共同的父组件,然后通过 props 一层一层传给子组件。问题是:如果组件层级很深,中间层会有大量 "不必要的 props 传递",代码冗余且难维护。
- useState:在每个组件里单独管理状态。问题是:如果多个组件需要共享状态(比如 Todo 列表),状态同步会变得复杂。
(2)useReducer+Context 的优势
- useReducer:像一个 "状态管理专家",把复杂的状态修改逻辑集中在一个纯函数里,避免状态修改逻辑散落在各个组件中。
- Context:实现 "跨层级状态共享",让任何层级的组件都能直接获取状态,无需层层传递 props。
这两者结合,既能高效管理状态,又能简化组件间通信,非常适合中小型应用的全局状态管理。
二、核心模块拆解:自定义 hook+Context,打造全局状态管理系统
(1)第一步:创建 Context,作为状态的 "管道"
javascript
// TodoContext.js
import { createContext } from 'react';
// 创建一个Context对象,初始值设为null
export const TodoContext = createContext(null);
Context 就像一个 "全局管道",所有组件都可以通过它获取状态和修改状态的方法。
(2)第二步:创建 reducer,定义状态修改规则
javascript
// reducers/todoReducer.js
// 纯函数:根据action修改状态
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, {
id: Date.now(), // 用时间戳生成唯一ID
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;
}
}
export default todoReducer;
这个 reducer 定义了 Todo 应用的三种操作:添加、切换完成状态、删除。注意它是纯函数,不修改原状态,而是返回新状态。
(3)第三步:封装自定义 hook,整合 useReducer 和 Context
javascript
// hooks/useTodos.js
import { useReducer } from 'react';
import todoReducer from '../reducers/todoReducer';
// 自定义hook:封装Todo状态管理逻辑
export function useTodos(initial = []) {
const [todos, dispatch] = useReducer(todoReducer, initial);
// 封装action creators,简化组件中的调用
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,
};
}
这个自定义 hook 做了两件事:
- 用
useReducer
初始化 Todo 状态和 dispatch 函数; - 封装了三个操作函数(
addTodo
、toggleTodo
、removeTodo
),让组件使用更方便。
(4)第四步:创建 Context 提供者,让状态 "流动" 起来
jsx
// App.jsx
import { TodoContext } from './TodoContext';
import { useTodos } from './hooks/useTodos';
import AddTodo from './components/AddTodo';
import TodoList from './components/TodoList';
function App() {
// 初始化Todo状态
const todosHook = useTodos([]);
return (
// 用Context.Provider包裹组件树,提供状态
<TodoContext.Provider value={todosHook}>
<h1>Todo App</h1>
<AddTodo />
<TodoList />
</TodoContext.Provider>
);
}
export default App;
这里的关键点是:把useTodos
返回的状态和方法通过TodoContext.Provider
的value
属性提供给所有子组件。
(5)第五步:在组件中使用全局状态
jsx
// components/AddTodo.jsx
import { useState } from 'react';
import { useTodoContext } from '../hooks/useTodoContext';
const AddTodo = () => {
const [text, setText] = useState('');
// 通过自定义hook获取全局状态和方法
const { addTodo } = useTodoContext();
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
addTodo(text.trim()); // 调用全局方法添加Todo
setText(''); // 清空输入框
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Add</button>
</form>
);
};
export default AddTodo;
jsx
// components/TodoList.jsx
import { useTodoContext } from '../hooks/useTodoContext';
const TodoList = () => {
// 通过自定义hook获取全局状态和方法
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>
);
};
export default TodoList;
注意这两个组件:它们完全不依赖 props 传递,而是通过useTodoContext
直接获取全局状态和操作方法。这样无论组件嵌套多深,都能轻松访问全局状态。




三、项目亮点解析:这些设计细节让代码更优雅
(1)自定义 hook 封装状态逻辑,组件只关心渲染
useTodos
这个自定义 hook 把 Todo 相关的状态逻辑(初始化、增删改查)全部封装起来,组件只需要调用它提供的方法,不需要关心内部实现。这符合 "关注点分离" 原则,让组件代码更简洁。
(2)统一的状态修改入口,保证状态一致性
所有状态修改都通过todoReducer
,这确保了状态修改逻辑集中且统一。如果需要修改 "添加 Todo" 的逻辑(比如增加验证),只需要改一处代码。
(3)避免重复的 Context 导入,简化组件代码
封装了useTodoContext
这个自定义 hook,组件里不需要每次都写useContext(TodoContext)
,减少重复代码:
javascript
// hooks/useTodoContext.js
import { TodoContext } from "../TodoContext";
import { useContext } from 'react';
export function useTodoContext() {
return useContext(TodoContext);
}
四、扩展思考:这个架构还能怎么优化?
(1)添加持久化存储
当前 Todo 数据只存在内存中,页面刷新就会丢失。可以用localStorage
实现持久化:
javascript
// hooks/useTodos.js
import { useReducer, useEffect } from 'react';
import todoReducer from '../reducers/todoReducer';
export function useTodos() {
// 从localStorage读取初始值
const initialTodos = JSON.parse(localStorage.getItem('todos')) || [];
const [todos, dispatch] = useReducer(todoReducer, initialTodos);
// 监听todos变化,保存到localStorage
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
// ...其余代码不变
}
(2)处理异步操作
如果需要处理异步操作(比如从 API 获取 Todo 数据),可以在自定义 hook 里添加useEffect
:
javascript
// hooks/useTodos.js
export function useTodos() {
const [todos, dispatch] = useReducer(todoReducer, []);
const [loading, setLoading] = useState(true);
// 初始化时从API获取数据
useEffect(() => {
fetch('/api/todos')
.then(res => res.json())
.then(data => {
dispatch({ type: 'INIT_TODOS', payload: data });
setLoading(false);
});
}, []);
// ...其余代码不变
}
五、总结:useReducer+Context 组合的核心价值
- 状态管理集中化:用 reducer 统一管理状态修改逻辑,避免逻辑分散。
- 跨层级通信简单化:通过 Context 直接获取状态,无需 props 层层传递。
- 代码可维护性提升:自定义 hook 封装状态逻辑,组件只关注渲染,职责清晰。