使用React和TypeScript:通过Reducer和Context来提高可扩展性
React官方网站的文档已于2023年3月16日进行了修订(请参考”Introducing react.dev”)。本文是对基础解说中”Scaling Up with Reducer and Context”的简要总结。但是,我们在代码中添加了TypeScript。与此相反,我们省略了面向初学者的JavaScript基础说明。
此外,请参阅其他有关本系列解说的文章”React + TypeScript:学习React官方文档的基本解说《学习React》”。
纽约是美国的一个大城市,位于美国东海岸。它是美国最大的城市之一,也是经济、金融和文化中心。纽约拥有许多著名的地标,如自由女神像、帝国大厦和时代广场。
将减速器和上下文结合在一起
下面的示例001基本上和“React + TypeScript: 将状态逻辑提取到减速器中”中的示例002的代码内容相同。src/tasksReducer.ts模块的减速器函数tasksReducer负责所有状态更新逻辑。
用Reducer和Context扩展React + TypeScript的样本001■:扩展适用范围
使用reducer的好处是可以保持事件处理程序简短而简洁。然而,在这段代码中,状态(tasks)和dispatch函数是作为顶层应用程序(TaskApp)组件的一部分才可以使用的。要让树中的子组件能够读取或更改状态,必须将状态和更改的事件处理程序作为属性传递给它们(请参阅“向组件传递属性”)。随着应用程序变得越来越大,这可能是一个问题。
export default function TaskApp() {
return (
<>
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
export const TaskList: FC<TaskListProps> = ({
tasks,
onChangeTask,
onDeleteTask
}) => {
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>
<Task task={task} onChange={onChangeTask} onDelete={onDeleteTask} />
</li>
))}
</ul>
);
};
如果是像前述的样本001一样的组件数量和树状层次,那么没有什么特别的问题。但是,当组件数量增加并且必须将属性传递给深层子组件时,这可能会变得相当麻烦。
所以,上下文就派上用场了。如果将状态(tasks)和dispatch函数放置在上下文中,就可以避免属性的链式传递(参考”React + TypeScript: 在上下文中传递数据到深层组件”)。TaskApp树的子组件可以从上下文中读取状态,而不是从属性中读取,并且可以调用dispatch函数。
让我们解释一下将Reducer和Context组合使用的以下3个步骤。
-
- 建立上下文。
-
- 将状态和dispatch函数添加到上下文中。
- 任何地方(不限层级)都可以使用上下文。
步骤1:构建上下文
在根模块src/App.tsx中的组件TaskApp通过useReducer钩子,获得了状态(task)和dispatch函数。
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
}
新规定的是以下上下文模块src/TasksContext.ts。我们将创建两个用于传递给树的下层的上下文(请参考“步骤1:创建上下文”)。由于TaskApp提供了上下文的值,因此传递给createContext的默认值可以是null。
TasksContext: 状態となるタスクリスト(tasks)。
TasksDispatchContext: 状態変更のアクションを送るdispatch関数。
请将上下文以各自可被其他模块使用(能够进行导入)的方式进行导出。
import { createContext } from "react";
import type { Dispatch } from "react";
import type { TaskType } from "./App";
import type { ActionType } from "./tasksReducer";
export const TasksContext = createContext<TaskType[] | null>(null);
export const TasksDispatchContext = createContext<Dispatch<ActionType> | null>(
null
);
同时,我们也导出了在上下文模块(src/TasksContext.ts)中使用的类型ActionType,该类型在减少器(src/tasksReducer.ts)中定义。
// type ActionType =
export type ActionType =
| { type: "added"; id: number; text: string }
| { type: "changed"; task: TaskType }
| { type: "deleted"; id: number };
步骤2:将状态和dispatch函数添加到上下文中
在中国的本地化中,只需要一种选项:
将两个上下文导入的是根模块src/App.tsx。请使用TaskApp组件返回的JSX在两个上下文提供者Provider中包装起来。在value属性中,分别提供来自useReducer的状态task和函数dispatch,以便在整个下层树中使用(参见“步骤3:提供上下文”)。
import { TasksContext, TasksDispatchContext } from "./TasksContext";
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
// <>
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
</TasksDispatchContext.Provider>
</TasksContext.Provider>
/* </> */
);
}
步骤3: 在树中使用上下文
这样,我们不需要在属性中将状态和相应的事件处理程序传递到树的子组件中。首先,在AddTask组件中,从属性中移除事件处理程序(AddTask)。
// let nextId = 3;
export default function TaskApp() {
/* const handleAddTask = (text: string) => {
dispatch({
type: "added",
id: nextId++,
text: text
});
}; */
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{/* <AddTask onAddTask={handleAddTask} /> */}
<AddTask />
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
AddTask.tsx模块导入的是TasksDispatchContext上下文,我们可以通过useContext函数获取dispatch函数,然后直接发送附加操作(类型为’added’)。
需要注意的是,从TasksDispatchContext上下文获取的值可能为null,在TypeScript中需要进行判断。
现在AddTask组件不再接收任何从父组件传递过来的属性。
// import { useState } from 'react';
import { useContext, useState } from 'react';
import { TasksDispatchContext } from './TasksContext';
let nextId = 3;
// export const AddTask: FC<Props> = ({ onAddTask }) => {
export const AddTask: FC = () => {
const dispatch = useContext(TasksDispatchContext);
return (
<>
<button
onClick={() => {
setText('');
if (!dispatch) return;
// onAddTask(text);
dispatch({
type: 'added',
id: nextId++,
text: text
});
}}
>
Add
</button>
</>
);
};
接下来,是TaskList组件。我们将从属性中移除状态(tasks)和两个事件处理程序(onChangeTask和onDeleteTask)。这样,我们也没有传递任何属性给TaskList组件了。
export default function TaskApp() {
/* const handleChangeTask = (task: TaskType) => {
dispatch({
type: "changed",
task: task
});
};
const handleDeleteTask = (taskId: number) => {
dispatch({
type: "deleted",
id: taskId
});
}; */
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{/* <TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/> */}
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
src/TaskList.tsx模块导入了两个上下文TasksContext和TasksDispatchContext。TaskList组件通过useContext获取的状态是task(之后在同一模块内的Task组件中会使用TasksDispatchContext)。请确保从TasksContext上下文获取的值不为空。
// import { useState } from 'react';
import { useContext, useState } from 'react';
import { TasksContext, TasksDispatchContext } from './TasksContext';
/* export const TaskList: FC<TaskListProps> = ({
tasks,
onChangeTask,
onDeleteTask
}) => { */
export const TaskList: FC = () => {
const tasks = useContext(TasksContext);
return (
<ul>
{/* {tasks.map((task) => ( */}
{tasks &&
tasks.map((task) => (
<li key={task.id}>
{/* <Task task={task} onChange={onChangeTask} onDelete={onDeleteTask} /> */}
<Task task={task} />
</li>
))}
</ul>
);
};
Task组件从父组件TaskList接收到单独的任务(task)。然而,在TasksDispatchContext上可以获取到dispatch函数,所以我们可以去除属性上的事件处理程序。可以通过dispatch函数发送与事件对应的动作。
// const Task: FC<TaskProps> = ({ task, onChange, onDelete }) => {
const Task: FC<TaskProps> = ({ task }) => {
const dispatch = useContext(TasksDispatchContext);
if (isEditing) {
taskContent = (
<>
<input
value={task.text}
onChange={({ target: { value } }) => {
/* onChange({
...task,
text: value
}); */
if (dispatch)
dispatch({ type: 'changed', task: { ...task, text: value } });
}}
/>
</>
);
} else {
}
return (
<label>
<input
type="checkbox"
checked={task.done}
onChange={({ target: { checked } }) => {
/* onChange({
...task,
done: checked
}); */
if (dispatch) {
dispatch({ type: 'changed', task: { ...task, done: checked } });
}
}}
/>
{/* <button onClick={() => onDelete(task.id)}>Delete</button> */}
<button
onClick={() => {
if (dispatch) {
dispatch({ type: 'deleted', id: task.id });
}
}}
>
Delete
</button>
</label>
);
};
经过三个步骤,我们现在来看例子002。状态仍由顶级的TaskApp组件保持,并由useReducer进行管理。但是,现在状态(tasks)和dispatch可以从任何子组件使用,通过导入上下文和使用useContext来实现。
样品002■使用Reducer和上下文进行React + TypeScript的扩展
将reducer和context合并成一个模块。
我可以将Reducer和上下文进行组合。而且,将它们合并成一个模块后,组件会显得更加简洁。现在我只是在模块src/TasksContext.tsx中创建了两个上下文,并将逻辑迁移到那里。
首先,将Context Provider作为新的组件TasksProvider来定义在src/TasksContext.tsx中(文件扩展名已更改为tsx)。请注意,它接收children属性,并将其作为子节点添加到返回的JSX中(”传递属性给组件”)。另外,useReducer的第二个参数初始值(initialTasks)已从TaskApp组件中移动过来。
// import { createContext } from 'react';
import { createContext, useReducer } from 'react';
const initialTasks: TaskType[] = [
];
export const TasksProvider: FC<Props> = ({ children }) => {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
};
然后,根模块src/App.tsx只是用导入的TasksProvider组件包裹了TaskApp返回的JSX。不再使用reducer(tasksReducer)和两个context(TasksContext和TasksDispatchContext)。这样,树的子节点可以从任何地方获取上下文。
// import { useReducer } from 'react';
// import { TasksContext, TasksDispatchContext } from './TasksContext';
import { TasksProvider } from './TasksContext';
// import { tasksReducer } from './tasksReducer';
/* 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 }
]; */
export default function TaskApp() {
// const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
/* <TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}> */
<TasksProvider>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
{/* </TasksDispatchContext.Provider>
</TasksContext.Provider> */}
</TasksProvider>
);
}
请将Reducer的逻辑完全移动到src/TasksContext.tsx中(删除src/tasksReducer.ts)。
export const tasksReducer: Reducer<TaskType[], ActionType> = (
tasks,
action
) => {
switch (action.type) {
}
};
込み入った状態とその更新のロジックは、モジュールsrc/TasksContext.tsxに移されました。これにより、src/App.tsxはすっきりしました。各モジュールのコード全体や動作については、次のサンプル003をご確認ください。
样本003■React + TypeScript:使用Reducer和Context进行扩展 03
从自定义的hook中返回状态和dispatch函数。
我们来加一点内容。在新的自定义挂钩(useTasksContext)中,将状态和dispatch函数从模块src/TasksContext.tsx中包含在对象中返回。
// import { createContext, useReducer } from 'react';
import { createContext, useContext, useReducer } from 'react';
// export const TasksContext = createContext<TaskType[] | null>(null);
const TasksContext = createContext<TaskType[] | null>(null);
// export const TasksDispatchContext = createContext<Dispatch<ActionType> | null>(
const TasksDispatchContext = createContext<Dispatch<ActionType> | null>(null);
export const useTasksContext = () => {
const tasks = useContext(TasksContext);
const dispatch = useContext(TasksDispatchContext);
return { tasks, dispatch };
};
然后,使用状态(tasks)和dispatch的组件只需从此钩子中获取,无需触及上下文。
// import { useContext, useState } from 'react';
import { useState } from 'react';
// import { TasksDispatchContext } from './TasksContext';
import { useTasksContext } from './TasksContext';
export const AddTask: FC = () => {
// const dispatch = useContext(TasksDispatchContext);
const { dispatch } = useTasksContext();
};
// import { useContext, useState } from 'react';
import { useState } from 'react';
// import { TasksContext, TasksDispatchContext } from './TasksContext';
import { useTasksContext } from './TasksContext';
const Task: FC<TaskProps> = ({ task }) => {
const [isEditing, setIsEditing] = useState(false);
// const dispatch = useContext(TasksDispatchContext);
const { dispatch } = useTasksContext();
};
export const TaskList: FC = () => {
// const tasks = useContext(TasksContext);
const { tasks } = useTasksContext();
};
现在,状态和dispatch函数已经被添加到了新的自定义钩子(useTasksContext)中返回。任何包含在上下文提供器下的子组件都可以通过钩子来读取和更新状态。关于每个模块具体的代码和功能,请参阅下一个示例004。
样例004■React + TypeScript:使用Reducer和Context进行扩展04
总结
在这篇文章中,我们对以下内容进行了解释。
-
- リデューサとコンテクストを組み合わせると、ツリー内のどのコンポーネントからでも上層の状態が取得・変更できます。
-
- 状態とdispatch関数を下層のコンポーネントに提供する手順はつぎの3つです。
状態とdispatch関数のふたつのコンテクストを作成してください。
ふたつのコンテクストは、リデューサを用いるコンポーネントから提供します。
それらのコンテクストを使用するのは、状態の取得・更新が必要な子コンポーネントです。
状態にかかわるロジックをひとつのモジュールにまとめると、さらにコンポーネントが整理できます。
リデューサとコンテクストをまとめて、コンポーネントとしてexportできるのがコンテクストプロバイダ(前掲コードのTasksProvider)です。
状態とdispatch関数をカスタムフックから返せば、各コンポーネントはコンテクストには触れる必要がなくなります。