在我之前的文章中,我已经分别对Context 和 Reducer 进行过详细的讲解,两种方法各有各的优势,那么,现在问你个问题,如果它们两种方法组合在一起,那又会发生什么呢?
今天,我将通过一个TodoList 实战,带你深入了解一下它们 “ 热血沸腾的组合技 ”。
一、Context 和 Reducer 的简单提要
1.1 Context : 跨层级通信的桥梁
Context 是一种无需手动传递 props ,就能让组件树中任意层级的组件直接访问祖先组件状态的方法。
它可以通过 createContext 创建上下文对象,再通过 Provider 将状态和方法传递给子组件,最后,子组件通过 useContext 就能使用上下文中的数据。
核心方法概括:
createContext(defaultValue):创建一个上下文对象,其中包含Provider和Consumer。Provider:为组件树提供共享状态,可以通过value属性来传递数据。useContext(context):在函数组件中使用上下文数据。
使用场景
当多个层级的组件需要共享相同的状态(如主题、登录状态、Todos列表)时,Context能有效减少props的冗余传递,简化组件间的通信逻辑。
1.2 Reducer :复杂状态管理的利器
useReducer 是 React 提供的 Hook,用于管理复杂的状态逻辑。
它通过一个 纯函数(reducer) 来定义状态的更新规则,结合 dispatch 方法派发 action,实现状态的响应式更新。
核心方法概括:
useReducer(reducer, initialState):React的Hook,用于返回当前状态和dispatch函数。reducer(state, action):纯函数,用于接收当前状态和action,返回一个新状态。dispatch(action):通过派发action事件,来触发状态更新。
使用场景
当状态逻辑较为复杂(如TodoList的添加、切换、删除操作),或需要依赖之前的状态值时,Reducer比useState更清晰且易于维护。
二、Context + Reducer 的组合实战
2.1 核心思路
上面我们已经分析了Context和Reducer各自的优点,所以,在TodoList案例中,两者的分工已经显而易见了。
Context作为“管道”,负责跨层级传递状态和操作方法,将状态和操作方法注入到组件树中。Reducer作为“控制台”,负责集中管理状态更新逻辑,统一处理所有状态变更请求。
这样用文字描述大家可能无法体会,下面,我将结合一个实战小案例,逐句来分析它们是如何协同工作的。
2.2 效果展示
为了更好地了解接下来的代码要做什么,我们先看看最终要实现的效果:
2.3 完整代码
为了不影响文章的观感,我将完整的代码放在文章最后面,有需要的读者朋友们可以自取。
三、超详细的代码分析
3.1 创建Context并注入状态
// App.jsx
import { TodoContext } from './TodoContext';
import { useTodos } from './hooks/useTodo';
function App() {
const todosHook = useTodos(); // 初始化Todos状态
return (
<TodoContext.Provider value={todosHook}>
<h1>Todo App</h1>
<AddTodo />
<TodoList />
</TodoContext.Provider>
);
}
效果说明:
- 这段代码中,我们通过
TodoContext.Provider将todosHook(包含状态和操作方法)注入到整个组件树中。 useTodos():这是一个自定义Hook,它封装了useReducer和业务逻辑,返回一个对象({ todos, addTodo, toggleTodo, removeTodo }),这个对象可以被Provider传递给所有子组件调用。
逐句分析
-
const todosHook = useTodos();
这句代码调用了useTodosHook,初始化Todos的状态和操作方法,useTodos内部使用了useReducer,会返回当前Todos列表和操作函数。 -
<TodoContext.Provider value={todosHook}>
这句代码将todosHook作为值注入到Provider中,所有子组件都可以通过useContext来访问这个值,无需我们手动传递props。
3.2 Reducer处理状态更新逻辑
// reducers/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;
效果说明
-
这段代码实现了根据
action.type来决定如何更新Todos列表。例如,添加任务时生成新ID,切换状态时修改done字段,删除任务时过滤掉指定ID。 -
第一行的
todoReducer是一个纯函数,它接收当前state和action,返回一个新状态,达到不直接修改原始数据的目的。
逐句分析
-
case 'Add_Todo'
当接收到Add_Todo类型的action时,它会将新任务添加到state数组的末尾。...state:用于展开原数组,保留已有任务。id: Date.now():为新任务生成唯一ID(基于当前时间戳)。text: action.text:从action.payload中提取任务文本。
-
case 'Toggle_Todo'
当接收到Toggle_Todo类型的action时,它会切换指定ID任务的完成状态。state.map(...):用来遍历所有任务,找到匹配ID的任务并更新其done字段。{ ...todo, done: !todo.done }:使用对象展开语法,创建新的任务对象,仅修改done字段。
-
case 'Remove_Todo'
当接收到Remove_Todo类型的action时,它会过滤掉指定ID的任务。state.filter(...):返回ID不匹配的任务数组,实现删除效果。
3.3 自定义Hook封装状态逻辑
// hooks/useTodo.js
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,
};
}
效果说明
这段代码将useReducer和业务逻辑封装成一个可复用的Hook,提供给组件使用,通过dispatch派发action,触发Reducer处理状态更新。
逐句分析
-
const [todos, dispatch] = useReducer(todoReducer, initial);useReducer(todoReducer, initial):初始化状态(initial默认为空数组),并绑定todoReducer作为状态更新规则。todos:当前Todos列表的状态值。dispatch:用于派发action的函数。
-
addTodo(text)dispatch({ type: 'Add_Todo', text }):当用户输入任务文本并提交时,派发一个Add_Todo类型的action,携带任务文本。text.trim():确保任务文本不为空(在组件中已处理)。
-
toggleTodo(id)dispatch({ type: 'Toggle_Todo', id }):点击任务时派发Toggle_Todo类型的action,携带任务ID,触发状态切换。
-
removeTodo(id)dispatch({ type: 'Remove_Todo', id }):点击删除按钮时派发Remove_Todo类型的action,携带任务ID,触发删除逻辑。
-
return { todos, addTodo, toggleTodo, removeTodo }- 返回一个对象,包含当前状态和操作方法,供
Provider注入到组件树中。
- 返回一个对象,包含当前状态和操作方法,供
3.4 子组件消费Context
// components/AddTodo.jsx
import { useTodoContext } from '../hooks/useTodoContext';
const 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
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Add</button>
</form>
);
};
效果说明
这段代码实现了表单提交时调用addTodo方法,将任务文本传递给Reducer处理,通过自定义Hook访问Context中的状态和方法,无需手动传递props。
逐句分析
-
const { addTodo } = useTodoContext();- 从
Context中提取addTodo方法,直接调用即可触发状态更新。
- 从
-
addTodo(text.trim())- 提取用户输入的文本,调用
addTodo方法,派发Add_Todo类型的action。
- 提取用户输入的文本,调用
-
setText('')- 清空输入框,重置表单状态。
3.5 渲染Todo列表
// components/TodoList.jsx
import { useTodoContext } from '../hooks/useTodoContext';
const 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>
);
};
效果说明
- 这段代码用于渲染Todos列表,提供切换状态和删除操作,并通过
useTodoContext()直接访问Context中的todos列表和操作方法。
逐句分析
-
const { todos, toggleTodo, removeTodo } = useTodoContext();- 从
Context中提取Todos列表和操作方法,无需父组件传递。
- 从
-
onClick={() => toggleTodo(todo.id)}- 点击任务时调用
toggleTodo,派发Toggle_Todo类型的action,切换任务状态。
- 点击任务时调用
-
onClick={() => removeTodo(todo.id)}- 点击删除按钮时调用
removeTodo,派发Remove_Todo类型的action,删除任务。
- 点击删除按钮时调用
-
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}- 根据任务的
done状态动态调整样式,已完成任务显示删除线。
- 根据任务的
四、总结
通过Context和Reducer的组合,我们实现了以下目标:
- 解耦状态管理:将状态逻辑与组件渲染分离,提升代码可维护性。
- 跨层级通信:无需手动传递props,通过
Context直接访问共享状态。 - 可预测的状态更新:通过
Reducer集中管理状态变更逻辑,确保行为一致。
这种模式特别适合中大型应用,既能避免“prop drilling”,又能保持状态更新的清晰可读性。在实际开发中,可以进一步结合自定义Hook和模块化设计,构建更复杂的业务逻辑。
附录:完整代码
App.jsx
import './App.css'
import { TodoContext } from './TodoContext'
import { useTodos } from './hooks/useTodo'
import AddTodo from './components/AddTodo'
import TodoList from './components/TodoList'
function App() {
const todosHook = useTodos()
return (
<TodoContext.Provider value={todosHook}>
<h1>Todo App</h1>
<AddTodo></AddTodo>
<TodoList></TodoList>
</TodoContext.Provider>
)
}
export default App
AddTodo.jsx
//components\AddTodo.jsx
import {useTodoContext} from '../hooks/useTodoContext'
import {useState } from 'react'
const 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 type="text" value={text} onChange={e => setText(e.target.value)} style={{margin : '10px'}}/>
<button type="submit">添加 </button>
</form>
</>
)
}
export default AddTodo
TodoList.jsx
//components\TodoList.jsx
import { useTodoContext } from '../hooks/useTodoContext'
const TodoList = () => {
const {
todos,
toggleTodo,
removeTodo
} = useTodoContext()
return (
<>
{
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)} style={{ 'margin-left': '10px', 'margin-top': '10px' }}>删除</button>
</li>
))
}
</>
)
}
export default TodoList
useTodoContext.js
// hooks/useTodoContext.js
import { TodoContext } from "../TodoContext";
import { useContext } from "react";
export function useTodoContext(){
return useContext(TodoContext);
}
useTodo.js
// hooks/useTodo.js
import { useReducer } from "react";
import todoReducer from "../reducers/todoReaducer";
export function useTodos(inital=[]){
const [todos,dispatch] = useReducer(todoReducer,inital);
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,
}
}
todoReducer.js
// reducers/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;
TodoContext.js
// TodoContext.js
import { createContext } from 'react';
export const TodoContext = createContext();