一、核心概念先理清 🔍
在动手拆解项目前,我们得先吃透两个核心工具的底层逻辑 ——useReducer 和 useContext 是如何配合实现全局状态管理的。
1. useReducer:状态管理的 "规则制定者"
-
核心作用:处理复杂状态逻辑,通过 "规则"(reducer)管理状态变化,比
useState更适合多状态联动的场景。 -
四大要素:
initialState:初始状态(比如空的 todo 列表[]);reducer:纯函数,接收当前状态和动作(action),返回新状态(核心!所有状态变化必须经它处理);dispatch:触发状态变化的 "传令兵",通过传递action(格式{type: '操作类型', payload: '数据'})调用 reducer;action:描述 "要做什么" 的指令,比如{type: 'ADD_TODO', text: '买牛奶'}。
2. useContext:组件通信的 "快捷通道"
-
核心作用:解决跨层级组件通信问题,避免 "props drilling"(props 层层传递的麻烦)。
-
三大步骤:
createContext:创建一个 "上下文容器"(比如TodoContext);Context.Provider:在父组件中 "提供" 数据(通过value属性),所有子组件(无论层级)都能访问;useContext:在子组件中 "获取" 上下文数据,直接使用父组件提供的状态和方法。
3. 黄金组合:useContext + useReducer
两者结合能实现全局应用级别的响应式状态管理:
useReducer负责 "管理状态变化的规则";useContext负责 "将状态和规则传递给所有需要的组件"。
二、项目实战:一步步搭建 Todo 应用 🛠️
步骤 1:创建上下文(TodoContext)
首先需要一个 "容器" 来存放全局状态,这一步通过 createContext 实现(通常单独放在 TodoContext.js 中):
import { createContext } from 'react';
// 创建上下文,默认值可设为 null 或初始结构
export const TodoContext = createContext(null);
👉 作用:定义一个全局可访问的数据容器,后续由 Provider 填充具体数据。
步骤 2:编写 reducer 规则(todoReducer.js)
reducer 是状态变化的 "指挥官",所有 todo 的增删改都要遵循它的规则:
// todoReducer.js
function todoReducer(state, action) {
switch(action.type) {
// 新增 todo:在原数组后追加新对象(保持状态不可变)
case 'ADD_TODO':
return [...state, {
id: Date.now(), // 用时间戳做唯一标识
text: action.text, // 从 action 中取输入文本
done: false // 初始为未完成
}];
// 切换 todo 状态:找到对应 id,反转 done 值
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id ? {...todo, done: !todo.done} : todo
);
// 删除 todo:过滤掉对应 id 的项
case 'REMOVE_TODO':
return state.filter(todo => todo.id !== action.id);
// 默认返回原状态(防止未知 action 破坏状态)
default:
return state;
}
}
export default todoReducer;
👉 细节:
- 所有操作都不直接修改原状态(比如不用
push而是用[...state]),这是 React 状态管理的核心原则(保持状态不可变); action.type是字符串常量,避免拼写错误(实际项目中可抽成常量文件)。
步骤 3:封装自定义 Hook(useTodos.js)
为了简化状态使用,我们用自定义 Hook 封装 useReducer 和状态操作方法:
// useTodos.js
import { useReducer } from 'react';
import todoReducer from '../reducers/todoReducer';
export function useTodos(initial = []) {
// 用 useReducer 关联 reducer 和初始状态,得到当前状态 todos 和传令兵 dispatch
const [todos, dispatch] = useReducer(todoReducer, initial);
// 封装操作方法:对外暴露直观的 API(无需关心 dispatch 和 action 细节)
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 };
}
👉 优势:
- 隐藏
dispatch的细节,组件只需调用addTodo('xxx')即可,不用手动写action; - 集中管理所有 todo 相关操作,后续修改逻辑只需改这里。
步骤 4:提供全局状态(App.jsx)
在根组件中用 TodoContext.Provider 把状态 "广播" 给所有子组件:
// App.jsx
import { TodoContext } from './TodoContext';
import { useTodos } from './hooks/useTodos';
import AddTodo from './components/AddTodo';
import TodoList from './components/TodoList';
function App() {
// 调用自定义 Hook 获取状态和方法
const todosHook = useTodos();
return (
// 通过 Provider 把数据"喂"给上下文,子组件就能访问了
<TodoContext.Provider value={todosHook}>
<h1>Todo App 📝</h1>
<AddTodo /> {/* 新增 todo 组件 */}
<TodoList /> {/* 展示 todo 列表组件 */}
</TodoContext.Provider>
);
}
👉 关键:value 属性传递的是 todosHook(包含 todos、addTodo 等),这意味着所有子组件都能拿到这些数据。
步骤 5:封装上下文访问 Hook(useTodoContext.js)
为了避免在每个组件中重复写 useContext(TodoContext),单独封装一个 Hook:
// useTodoContext.js
import { useContext } from 'react';
import { TodoContext } from '../TodoContext';
// 直接返回上下文数据,简化组件中的使用
export function useTodoContext() {
return useContext(TodoContext);
}
👉 作用:复用逻辑,后续若修改 Context 名称,只需改这一个文件。
步骤 6:实现功能组件(AddTodo 和 TodoList)
① AddTodo:新增 todo 组件
// AddTodo/index.jsx
import { useState } from 'react';
import { useTodoContext } from '../../hooks/useTodoContext';
const AddTodo = () => {
// 本地状态:管理输入框文本
const [text, setText] = useState('');
// 获取全局方法:addTodo
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)} // 实时更新输入文本
placeholder="请输入任务..."
/>
<button type="submit">添加 ✚</button>
</form>
);
};
👉 亮点:本地状态(输入框文本)和全局状态(todo 列表)分离,职责清晰。
② TodoList:展示和操作 todo 列表
// TodoList/index.jsx
import { useTodoContext } from '../../hooks/useTodoContext';
const TodoList = () => {
// 获取全局状态和方法:todos、toggleTodo、removeTodo
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>
{/* 点击按钮删除 todo */}
<button onClick={() => removeTodo(todo.id)}>删除 🗑️</button>
</li>
))}
</ul>
);
};
👉 亮点:通过 todo.done 动态控制样式,直观反映任务状态;点击事件直接调用全局方法,无需关心状态更新细节。
三、总结:这套模式好在哪? 🚀
-
状态管理清晰:所有状态变化规则集中在
todoReducer中,可预测、易调试; -
组件通信高效:通过
useContext实现跨层级数据共享,不用手动传递 props; -
逻辑复用性强:自定义 Hook(
useTodos、useTodoContext)封装重复逻辑,组件更简洁; -
可扩展性高:若需新增功能(比如编辑 todo),只需在 reducer 中加一个
case 'EDIT_TODO',再在组件中调用即可。
通过这个 Todo 应用,我们能清晰看到 useReducer 和 useContext 如何分工协作:前者定规则,后者传数据,两者结合让全局状态管理变得简单可控。下次遇到复杂状态场景,不妨试试这套组合拳哦! 💪