Reducer 允许您整合组件的状态更新逻辑。Context 允许您将信息传递给其他组件的深层。您可以将 Reducer 和 Context 结合起来管理复杂屏幕的状态。
您将学习
- 如何将 Reducer 与 Context 结合
- 如何避免通过 props 传递状态和 dispatch
- 如何将 Context 和状态逻辑保存在单独的文件中
将 Reducer 与 Context 结合
在此示例中,来自Reducer 简介,状态由 Reducer 管理。Reducer 函数包含所有状态更新逻辑,并在该文件底部声明
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer( tasksReducer, initialTasks ); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId }); } return ( <> <h1>Day off in Kyoto</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [...tasks, { id: action.id, text: action.text, done: false }]; } case 'changed': { return tasks.map(t => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter(t => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Philosopher’s Path', done: true }, { id: 1, text: 'Visit the temple', done: false }, { id: 2, text: 'Drink matcha', done: false } ];
Reducer 有助于保持事件处理程序简短而简洁。但是,随着应用程序的增长,您可能会遇到另一个困难。 目前,tasks
状态和 dispatch
函数仅在顶级 TaskApp
组件中可用。 要让其他组件读取任务列表或更改它,您必须明确地向下传递当前状态和更改它的事件处理程序作为 props。
例如,TaskApp
将任务列表和事件处理程序传递给 TaskList
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
而 TaskList
将事件处理程序传递给 Task
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
在这个小示例中,这很有效,但如果中间有数十或数百个组件,则传递所有状态和函数可能会非常令人沮丧!
这就是为什么,作为通过 props 传递它们的替代方法,您可能希望将 tasks
状态和 dispatch
函数放入 Context 中。 这样,树中 TaskApp
下方的任何组件都可以读取任务并调度操作,而无需重复的“prop drilling”。
以下是将 Reducer 与 Context 结合的方法
- 创建Context。
- 将状态和 dispatch 放入 Context。
- 使用树中任何位置的 Context。
步骤 1:创建 Context
useReducer
Hook 返回当前的 tasks
和 dispatch
函数,使您可以更新它们
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
要将它们传递给树,您将创建两个单独的 Context
TasksContext
提供当前任务列表。TasksDispatchContext
提供允许组件调度操作的函数。
从单独的文件中导出它们,以便您以后可以从其他文件中导入它们
import { createContext } from 'react'; export const TasksContext = createContext(null); export const TasksDispatchContext = createContext(null);
在这里,您将 null
作为默认值传递给两个上下文。实际值将由 TaskApp
组件提供。
步骤 2:将状态和调度放入上下文
现在,您可以在 TaskApp
组件中导入这两个上下文。获取 useReducer()
返回的 tasks
和 dispatch
并将它们提供给下面的整个树
import { TasksContext, TasksDispatchContext } from './TasksContext.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ...
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
...
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
目前,您可以通过 props 和上下文传递信息
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksContext, TasksDispatchContext } from './TasksContext.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer( tasksReducer, initialTasks ); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId }); } return ( <TasksContext.Provider value={tasks}> <TasksDispatchContext.Provider value={dispatch}> <h1>Day off in Kyoto</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </TasksDispatchContext.Provider> </TasksContext.Provider> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [...tasks, { id: action.id, text: action.text, done: false }]; } case 'changed': { return tasks.map(t => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter(t => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Philosopher’s Path', done: true }, { id: 1, text: 'Visit the temple', done: false }, { id: 2, text: 'Drink matcha', done: false } ];
在下一步中,您将删除 prop 传递。
步骤 3:在树中的任何位置使用上下文
现在您不需要将任务列表或事件处理程序向下传递给树
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>
相反,任何需要任务列表的组件都可以从 TaskContext
中读取它
export default function TaskList() {
const tasks = useContext(TasksContext);
// ...
要更新任务列表,任何组件都可以从上下文中读取 dispatch
函数并调用它
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useContext(TasksDispatchContext);
// ...
return (
// ...
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Add</button>
// ...
TaskApp
组件不会向下传递任何事件处理程序,TaskList
也不会将任何事件处理程序传递给 Task
组件。 每个组件都会读取它需要的上下文
import { useState, useContext } from 'react'; import { TasksContext, TasksDispatchContext } from './TasksContext.js'; export default function TaskList() { const tasks = useContext(TasksContext); return ( <ul> {tasks.map(task => ( <li key={task.id}> <Task task={task} /> </li> ))} </ul> ); } function Task({ task }) { const [isEditing, setIsEditing] = useState(false); const dispatch = useContext(TasksDispatchContext); let taskContent; if (isEditing) { taskContent = ( <> <input value={task.text} onChange={e => { dispatch({ type: 'changed', task: { ...task, text: e.target.value } }); }} /> <button onClick={() => setIsEditing(false)}> Save </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Edit </button> </> ); } return ( <label> <input type="checkbox" checked={task.done} onChange={e => { dispatch({ type: 'changed', task: { ...task, done: e.target.checked } }); }} /> {taskContent} <button onClick={() => { dispatch({ type: 'deleted', id: task.id }); }}> Delete </button> </label> ); }
状态仍然“存在于”顶级 TaskApp
组件中,由 useReducer
管理。 但它的 tasks
和 dispatch
现在可以通过导入和使用这些上下文供树中的每个组件使用。
将所有接线移至单个文件
您不必这样做,但您可以通过将 reducer 和上下文移到一个文件中来进一步整理组件。目前,TasksContext.js
只包含两个上下文声明
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
这个文件即将变得拥挤!您将把 reducer 移到同一个文件中。然后,您将在同一个文件中声明一个新的 TasksProvider
组件。此组件将把所有部分连接在一起
- 它将使用 reducer 管理状态。
- 它将为下面的组件提供两种上下文。
- 它将将
children
作为 prop,以便您可以将 JSX 传递给它。
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
这将从您的 TaskApp
组件中删除所有复杂性和接线
import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksProvider } from './TasksContext.js'; export default function TaskApp() { return ( <TasksProvider> <h1>Day off in Kyoto</h1> <AddTask /> <TaskList /> </TasksProvider> ); }
您还可以从 TasksContext.js
中导出*使用*上下文的函数
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
当组件需要读取上下文时,它可以通过这些函数来完成
const tasks = useTasks();
const dispatch = useTasksDispatch();
这不会以任何方式改变行为,但它可以让您稍后进一步拆分这些上下文,或者向这些函数添加一些逻辑。 现在所有上下文和 reducer 接线都在 TasksContext.js
中。这使得组件保持整洁和清晰,专注于它们显示的内容,而不是它们从哪里获取数据:
import { useState } from 'react'; import { useTasks, useTasksDispatch } from './TasksContext.js'; export default function TaskList() { const tasks = useTasks(); return ( <ul> {tasks.map(task => ( <li key={task.id}> <Task task={task} /> </li> ))} </ul> ); } function Task({ task }) { const [isEditing, setIsEditing] = useState(false); const dispatch = useTasksDispatch(); let taskContent; if (isEditing) { taskContent = ( <> <input value={task.text} onChange={e => { dispatch({ type: 'changed', task: { ...task, text: e.target.value } }); }} /> <button onClick={() => setIsEditing(false)}> Save </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Edit </button> </> ); } return ( <label> <input type="checkbox" checked={task.done} onChange={e => { dispatch({ type: 'changed', task: { ...task, done: e.target.checked } }); }} /> {taskContent} <button onClick={() => { dispatch({ type: 'deleted', id: task.id }); }}> Delete </button> </label> ); }
您可以将 TasksProvider
视为屏幕的一部分,它知道如何处理任务,将 useTasks
视为读取任务的方式,将 useTasksDispatch
视为从树中的任何下方组件更新任务的方式。
随着应用程序的增长,您可能会有许多像这样的上下文-reducer 对。这是一种强大的方法,可以在您希望在树的深处访问数据时,扩展您的应用程序并向上提升状态,而无需太多工作。
回顾
- 您可以将 reducer 与上下文结合使用,以允许任何组件读取和更新其上面的状态。
- 要向下面的组件提供状态和调度函数
- 创建两个上下文(一个用于状态,一个用于调度函数)。
- 从使用 reducer 的组件提供两种上下文。
- 从需要读取上下文的组件中使用任何一种上下文。
- 您可以通过将所有接线移到一个文件中来进一步整理组件。
- 您可以导出一个像
TasksProvider
这样的组件,它提供上下文。 - 您还可以导出自定义 Hook,如
useTasks
和useTasksDispatch
来读取它。
- 您可以导出一个像
- 您的应用程序中可以有许多像这样的上下文-reducer 对。