前言
还在为React的状态管理头疼吗?useState满天飞,props层层传递,状态逻辑散落各处?今天带你用React原生的Hook组合拳——useReducer + useContext,打造一个既优雅又实用的全局状态管理方案。
不需要Redux,不需要Zustand,就用React自带的这两个Hook,我们就能实现一个功能完整的Todo应用状态管理系统。
项目结构一览
在开始之前,我们先看看整个项目的文件结构。这样能让你对项目有个全局的认识:
todo-app/
├── public/
│ └── index.html
├── src/
│ ├── components/ # UI组件目录
│ │ ├── AddTodo.jsx # 添加待办事项组件
│ │ └── TodoList.jsx # 待办事项列表组件
│ ├── hooks/ # 自定义Hook目录
│ │ ├── useTodos.js # Todo状态管理Hook
│ │ └── useTodoContext.js # Context消费Hook
│ ├── reducers/ # Reducer目录
│ │ └── todoReducer.js # Todo状态变更逻辑
│ ├── App.jsx # 根组件
│ ├── App.css # 应用样式
│ ├── TodoContext.js # 全局Context定义
│ ├── main.jsx # 应用入口
│ └── index.css # 全局样式
└── package.json
这个结构看起来是不是很清爽?我们按照功能模块来组织代码:
components/存放UI组件hooks/存放自定义Hookreducers/存放状态管理逻辑- 根目录放应用的主要文件
核心思路:Hook组合的艺术
在深入代码之前,先理解一下我们的整体架构:
- useReducer:负责响应式状态管理,用纯函数定义状态变更规则
- useContext:负责跨组件通信,让状态在组件树中自由流动
- 自定义Hook:将状态逻辑与UI渲染分离,让组件专注于显示
这三者组合起来,就是一个完整的全局应用级别响应式状态管理方案。
第一步:定义状态变更规则
首先我们需要一个reducer来管理Todo的状态变更。这个函数必须是纯函数,相同输入永远产生相同输出:
// todoReducer.js
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;
}
}
export default todoReducer;
这个reducer定义了三种操作:
- ADD_TODO:新增待办事项,用时间戳作为唯一ID
- TOGGLE_TODO:切换完成状态,用map遍历找到对应项目
- REMOVE_TODO:删除待办事项,用filter过滤掉指定ID
第二步:创建上下文通信管道
Context就像是组件间的一条高速公路,让数据可以跨越组件层级自由传递:
// TodoContext.js
import {
createContext,
useContext
} from "react";
// 创建上下文
export const TodoContext = createContext(null);
第三步:封装自定义Hook
这是整个方案的核心,我们把状态逻辑都封装在这个自定义Hook里:
// useTodos.js
import { useReducer } from "react"
import todoReducer from "../reducers/todoReducer"
// 使用参数默认值,支持初始化数据
export function useTodos(inital = []){
const [todos, dispatch] = useReducer(todoReducer, inital);
// 封装dispatch操作,让组件使用更简单
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
}
}
export default useTodos;
这个Hook做了几件关键的事:
- 用useReducer管理状态
- 封装了dispatch操作,让API更友好
- 返回一个包含状态和方法的对象
第四步:创建Context消费Hook
为了让其他组件更方便地使用Context,我们再封装一个Hook:
// useTodoContext.js
import { useContext } from 'react';
import { TodoContext } from '../TodoContext';
export function useTodoContext(){
return useContext(TodoContext);
}
第五步:在根组件中提供状态
现在在App组件中把所有东西串联起来:
// App.jsx
import { useState } from 'react'
import './App.css'
import { TodoContext } from './TodoContext';
import { useTodos } from './hooks/useTodos';
import AddTodo from './components/AddTodo';
import TodoList from './components/TodoList';
function App() {
// 创建todos状态管理实例
const todosHook = useTodos();
return (
// 通过Provider将状态注入到组件树
<TodoContext.Provider value={todosHook}>
<h1>Todo App</h1>
<AddTodo />
<TodoList />
</TodoContext.Provider>
)
}
export default App
第六步:在子组件中消费状态
添加Todo组件
// AddTodo.jsx
import { useState } from "react";
import { useTodoContext } from "../hooks/useTodoContext";
const AddTodo = () => {
// 私有状态,管理输入框
const [text, setText] = useState('');
// 从Context获取全局状态操作方法
const {addTodo} = useTodoContext();
const handleSubmit = e => {
e.preventDefault();
if(text.trim()){
addTodo(text.trim());
setText(''); // 清空输入框
}
}
return(
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={e => setText(e.target.value)}
placeholder="输入待办事项..."
/>
<button type="submit">Add</button>
</form>
)
}
export default AddTodo;
Todo列表组件
// TodoList.jsx
import { useTodoContext } from "../hooks/useTodoContext";
const TodoList = () => {
// 从Context获取状态和操作方法
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',
cursor: 'pointer'
}}
>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>
Remove
</button>
</li>
))}
</ul>
)
}
export default TodoList;
方案优势分析
1. 响应式状态管理
useReducer提供了强大的状态管理能力:
- 状态变更逻辑集中管理
- 纯函数保证状态变更的可预测性
- 复杂状态逻辑处理更加清晰
2. 跨层级通信
useContext解决了props drilling问题:
- 状态可以跨越任意组件层级传递
- 不需要在中间组件中传递无关的props
- 组件结构更加清晰
3. 关注点分离
自定义Hook实现了完美的关注点分离:
- 组件专注于UI渲染
- Hook专注于状态管理
- 逻辑复用变得简单
4. 类型友好
这套方案对TypeScript非常友好,可以很容易地添加类型定义。
扩展思考
这套方案不仅适用于Todo应用,还可以扩展到:
- 主题管理:light/dark主题切换
- 用户登录:用户信息、登录状态管理
- 购物车:商品添加、删除、数量管理
- 表单状态:复杂表单的状态管理
只需要:
- 定义对应的reducer
- 创建对应的Context
- 封装自定义Hook
小结
通过useReducer + useContext的组合,我们用React原生的能力实现了一个功能完整的状态管理方案。这套方案既保持了代码的简洁性,又提供了足够的灵活性。
相比于引入第三方状态管理库,这种方案有几个明显优势:
- 零依赖,基于React原生API
- 学习成本低,不需要额外的概念
- 体积小,不会增加bundle大小
- 调试友好,可以直接使用React DevTools
当然,如果你的应用状态管理需求非常复杂,可能还是需要考虑Redux、Zustand等专业的状态管理库。但对于大多数中小型应用,这套原生方案已经完全够用了。
最后想说,技术选型没有银弹,适合的才是最好的。React Hook的组合威力远比我们想象的强大,多尝试、多思考,你会发现更多有趣的用法!