第三章 - 状态管理
使用 Reducer 和 Context 拓展你的应用
Reducer 可以整合组件的状态更新逻辑。Context 可以将信息深入传递给其他组件。你可以组合使用它们来共同管理一个复杂页面的状态。
结合使用 reducer 和 context
在 reducer 介绍 的例子里面,状态被 reducer 所管理。reducer 函数包含了所有的状态更新逻辑并在此文件的底部声明:
react
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
将 一系列 task 和事件处理程序传递给 TaskList
:
react
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
TaskList
将事件处理程序传递给 Task
:
react
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
在像这样的小示例里这样做没什么问题,但是如果你有成千上百个组件,传递所有状态和函数可能会非常麻烦!
这就是为什么,比起通过props传递它们,你可能想把tasks状态和dispatch函数都放入context。这样,所有的TaskApp组件树之下的组件都不必一直往下传props而可以直接读取tasks 和 dispatch 函数。
下面将介绍如何结合使用 reducer 和 context:
- 创建context
- 将state 和 dispatch 放入 context
- 在组件树的任何地方使用context
第一步:创建context
useReducer
返回当前的 tasks
和 dispatch
函数来让你更新它们:
react
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
为了将他们从组件树往下传,你将创建两个不同的context:
TasksContext
提供当前的 tasks 列表。TasksDispatchContext
提供了一个函数可以让组件分发动作。
将它们从单独的文件导出,以便以后可以从其他文件导入它们:
react
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
在这里,你把 null
作为默认值传递给两个 context。实际值是由 TaskApp
组件提供的。
第二步:将state 和 dispatch函数 放入 context
现在,你可以将所有的context导入 TaskApp 组件。获取 useReducer() 返回 tasks 和 dispatch 并将它们提供给整个组件树。
react
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>
);
}
第三步:在组件树中的任何地方使用context
现在你不需要将tasks和事件处理程序在组件树中传递:
react
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>
相反,任何需要tasks的组件都可以从TaskContext中读取它:
react
export default function TaskList() {
const tasks = useContext(TasksContext)
}
任何组件都可以从context中读取dispatch函数并调用它,从而更新任务列表:
react
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
也不会。每个组件都会读取它需要的 context:
react
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>
);
}
state 仍然 "存在于" 顶层 Task
组件中,由 useReducer
进行管理 。不过,组件树里的组件只要导入这些 context 之后就可以获取 tasks
和 dispatch
。
将相关逻辑迁移到一个文件当中
这不是必须的,但你可以通过将reducer 和 context 移动到单个文件中来进一步整理组件。目前,"TasksContext.js"仅包含两个context声明:
react
import { createContext } from 'react'
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
来给这个文件添加更多代码!将 reducer 移动到此文件中,然后声明一个新的 TasksProvider
组件。此组件将所有部分连接在一起:
- 它将管理 reducer 的状态。
- 它将提供现有的 context 给组件树。
- 它将 把
children
作为 prop,所以你可以传递 JSX。
react
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
组件更加直观:
react
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
中导出使用 context 的函数:
react
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
组件可以通过以下函数读取 context:
react
const tasks = useTasks();
const dispatch = useTasksDispatch();
这不会改变任何行为,但它会允许你之后进一步分割这些 context 或向这些函数添加一些逻辑。现在所有的 context 和 reducer 连接部分都在 TasksContext.js
中。这保持了组件的干净和整洁,让我们专注于它们显示的内容,而不是它们从哪里获得数据:
react
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
视为页面的一部分,它知道如何处理 tasks。useTasks
用来读取它们,useTasksDispatch
用来从组件树下的任何组件更新它们。
随着应用的增长,你可能会有许多这样的 context 和 reducer 的组合。这是一种强大的拓展应用并 提升状态 的方式,让你在组件树深处访问数据时无需进行太多工作。
摘要
- 你可以将reducer和context结合,让任何组件读取和更新它的状态
- 为子组件提供state 和 dispatch函数:
- 创建两个context(一个用于state,一个用于dispatch函数)
- 让组件的context使用reducer
- 使用组件中需要读取的context
- 你可以将所有传递信息的代码移动到单个文件来进一步整理组件
- 你可以导出一个像
TasksProvider
可以提供 context 的组件。 - 你也可以导出像
useTasks
和useTasksDispatch
这样的自定义 Hook。
- 你可以导出一个像
- 你可以在你的应用程序中大量使用context 和 reducer 的组合