使用 reducer 和 context 进行扩展
Reducers 让您可以整合组件的状态更新逻辑。 上下文使您可以将信息深入传递给其他组件。 您可以将 reducers 和上下文结合在一起来管理复杂屏幕的状态。
你将学习
- 如何将 reducer 与 context 结合起来
- 如何避免通过 props 传递 state 和 dispatch
- 如何将上下文和状态逻辑保存在单独的文件中
将 reducer 与 context 结合起来
在这个介绍 reducer 的例子中,状态由 reducer 管理。 reducer 函数包含所有状态更新逻辑,并在此文件的底部声明:
TaskList.js
import { useState } from 'react';
export default function TaskList({
tasks,
onChangeTask,
onDeleteTask
}) {
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
</li>
))}
</ul>
);
}
function Task({ task, onChange, onDelete }) {
const [isEditing, setIsEditing] = useState(false);
let taskContent;
if (isEditing) {
taskContent = (
<>
<input
value={task.text}
onChange={e => {
onChange({
...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 => {
onChange({
...task,
done: e.target.checked
});
}}
/>
{taskContent}
<button onClick={() => onDelete(task.id)}>
Delete
</button>
</label>
);
}
AddTask.js
import { useState } from 'react';
export default function AddTask({ onAddTask }) {
const [text, setText] = useState('');
return (
<>
<input
placeholder="Add task"
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
onAddTask(text);
}}>Add</button>
</>
)
}
App.js
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 有助于使事件处理程序简短明了。 但是,随着应用程序的增长,您可能会遇到另一个困难。 目前,任务状态和调度功能仅在顶级 TaskApp 组件中可用。 要让其他组件读取任务列表或更改它,您必须显式传递当前状态和更改它的事件处理程序作为props。
例如,TaskApp 将任务列表和事件处理程序传递给 TaskList:
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
TaskList 将事件处理程序传递给 Task:
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
在像这样的小示例中,这很有效,但如果中间有数十或数百个组件,传递所有状态和函数可能会非常令人沮丧!
这就是为什么,作为通过 props 传递它们的替代方法,您可能希望将任务状态和调度函数都放入上下文中。 这样,树中 TaskApp 下方的任何组件都可以读取任务并分派操作,而无需重复的“props钻取”。
以下是如何将 reducer 与上下文结合起来:
- 创建上下文。
- 将状态和调度置于上下文中。
- 在树中的任何位置使用上下文。
第 1 步:创建上下文
useReducer Hook 返回当前任务和允许您更新它们的调度函数:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
要将它们传递到树中,您将创建两个单独的上下文:
- TasksContext 提供当前的任务列表。
- TasksDispatchContext 提供了让组件分派动作的功能。
从单独的文件中导出它们,以便以后可以从其他文件中导入它们:
TaskList.js
import { useState } from 'react';
export default function TaskList({
tasks,
onChangeTask,
onDeleteTask
}) {
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
</li>
))}
</ul>
);
}
function Task({ task, onChange, onDelete }) {
const [isEditing, setIsEditing] = useState(false);
let taskContent;
if (isEditing) {
taskContent = (
<>
<input
value={task.text}
onChange={e => {
onChange({
...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 => {
onChange({
...task,
done: e.target.checked
});
}}
/>
{taskContent}
<button onClick={() => onDelete(task.id)}>
Delete
</button>
</label>
);
}
AddTask.js
import { useState } from 'react';
export default function AddTask({ onAddTask }) {
const [text, setText] = useState('');
return (
<>
<input
placeholder="Add task"
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
onAddTask(text);
}}>Add</button>
</>
)
}
TasksContext.js
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
App.js
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 }
];
在这里,您将 null 作为默认值传递给两个上下文。 实际值将由 TaskApp 组件提供。
第 2 步:将状态和调度放入上下文中
现在您可以在 TaskApp 组件中导入这两个上下文。 获取 useReducer() 返回的任务和分派,并将它们提供给下面的整个树:
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和上下文传递信息
在下一步中,您将删除props传递。
第 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);
// ...
要更新任务列表,任何组件都可以从上下文中读取调度函数并调用它:
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 组件传递任何事件处理程序。 每个组件读取它需要的上下文:
App.js
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
);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</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);
}
}
}
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 }
];
TasksContext.js
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
AddTask.js
import { useState, useContext } from 'react';
import { TasksDispatchContext } from './TasksContext.js';
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useContext(TasksDispatchContext);
return (
<>
<input
placeholder="Add task"
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Add</button>
</>
);
}
let nextId = 3;
TaskList.js
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 管理。 但是通过导入和使用这些上下文,它的任务和调度现在可用于树中下面的每个组件。
将所有布线移动到一个文件中
您不必这样做,但您可以通过将 reducer 和上下文移动到一个文件中来进一步整理组件。 目前,TasksContext.js 只包含两个上下文声明:
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
这个文件即将变得拥挤! 您将把 reducer 移动到同一个文件中。 然后您将在同一个文件中声明一个新的 TasksProvider 组件。 该组件会将所有部分连接在一起:
- 它将使用reducer管理状态。
- 它将为下面的组件提供这两种上下文。
- 它会将孩子作为props,因此您可以将 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 组件的所有复杂性和连接:
App.js
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
import { createContext, useReducer } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</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);
}
}
}
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 }
];
AddTask.js
import { useState, useContext } from 'react';
import { TasksDispatchContext } from './TasksContext.js';
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useContext(TasksDispatchContext);
return (
<>
<input
placeholder="Add task"
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Add</button>
</>
);
}
let nextId = 3;
TaskList.js
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>
);
}
您还可以导出使用 TasksContext.js 中的上下文的函数:
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
当一个组件需要读取上下文时,它可以通过这些函数来完成:
const tasks = useTasks();
const dispatch = useTasksDispatch();
这不会以任何方式改变行为,但它允许您稍后进一步拆分这些上下文或向这些函数添加一些逻辑。 现在所有的上下文和 reducer 连接都在 TasksContext.js 中。 这使组件保持干净整洁,专注于它们显示的内容而不是它们获取数据的位置:
App.js
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
import { createContext, useContext, useReducer } from 'react';
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
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);
}
}
}
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 }
];
AddTask.js
import { useState } from 'react';
import { useTasksDispatch } from './TasksContext.js';
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useTasksDispatch();
return (
<>
<input
placeholder="Add task"
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Add</button>
</>
);
}
let nextId = 3;
TaskList.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 视为屏幕的一部分,它知道如何处理任务,使用 Tasks 作为读取任务的方式,并使用 TasksDispatch 作为从树下方的任何组件更新任务的方式。
像 useTasks 和 useTasksDispatch 这样的函数被称为自定义Hooks。 如果您的函数名称以 use 开头,则该函数被视为自定义 Hook。 这使您可以在其中使用其他 Hook,例如 useContext。
随着您的应用程序的增长,您可能会有许多这样的context-reducer对。 这是一种强大的方法,可以在您想要访问树深处的数据时无需太多工作即可扩展您的应用程序并提升状态。
回顾
- 您可以将 reducer 与上下文结合起来,让任何组件读取和更新其上方的状态。
- 为以下组件提供状态和调度功能:
- 创建两个上下文(用于状态和调度函数)。
- 从使用 reducer 的组件中提供两个上下文。
- 使用来自需要读取它们的组件的任一上下文。
- 您可以通过将所有布线移动到一个文件中来进一步整理组件。
- 您可以导出提供上下文的组件,例如 TasksProvider。
- 您还可以导出自定义 Hooks,例如 useTasks 和 useTasksDispatch 来读取它。
- 您的应用程序中可以有很多这样的context-reducer对。
网友评论