跳到主要内容

使用 Reducer 和 Context

使用 Reducer 和 Context 拓展应用

  • Reducer 可以整合组件的状态更新逻辑。
  • Context 可以将信息深入传递给其他组件。
  • 你可以组合使用它们来共同管理一个复杂页面的状态。
App.jsx
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 },
]
AddTask.jsx
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>
</>
)
}
TaskList.jsx
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>
)
}
  • Reducer 有助于保持事件处理程序的简短明了。
  • 但随着应用规模越来越庞大,你就可能会遇到别的困难。
  • 目前,tasks 状态和 dispatch 函数仅在顶级 TaskApp 组件中可用。
  • 要让其他组件读取任务列表或更改它,你必须显式 传递 当前状态和事件处理程序,将其作为 props。
例如,TaskApp 将 一系列 task 和事件处理程序传递给 TaskList:
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
TaskList 将事件处理程序传递给 Task:
<Task task={task} onChange={onChangeTask} onDelete={onDeleteTask} />

在像这样的小示例里这样做没什么问题,但是如果你有成千上百个组件,传递所有状态和函数可能会非常麻烦!

  • 这就是为什么,比起通过 props 传递它们,你可能想把 tasks 状态和 dispatch 函数都 放入 context。
  • 这样,所有的在 TaskApp 组件树之下的组件都不必一直往下传 props 而可以直接读取 tasks 和 dispatch 函数。

如何结合使用 reducer 和 context:

  1. 创建 context。
  2. 将 state 和 dispatch 放入 context。
  3. 在组件树的任何地方 使用 context。

第一步: 创建 context

useReducer 返回当前的 tasks 和 dispatch 函数来让你更新它们:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks)

为了将它们从组件树往下传,你将 创建 两个不同的 context:

  • TasksContext 提供当前的 tasks 列表。
  • TasksDispatchContext 提供了一个函数可以让组件分发动作。

将它们从单独的文件导出,以便以后可以从其他文件导入它们:

TasksContext.jsx 创建上下文文件
import { createContext } from 'react'

export const TasksContext = createContext(null)
export const TasksDispatchContext = createContext(null)

第二步: 将 state 和 dispatch 函数 放入 context

现在,你可以将所有的 context 导入 TaskApp 组件。获取 useReducer() 返回的 tasks 和 dispatch 并将它们 提供 给整个组件树:

提供 state 和 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}>
...
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>
)
}

第三步: 在组件树中的任何地方使用 context

任何需要 tasks 的组件都可以从 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 也不会。每个组件都会读取它需要的 context:
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
  • state 仍然 “存在于” 顶层 Task 组件中,由 useReducer 进行管理。
  • 不过,组件树里的组件只要导入这些 context 之后就可以获取 tasks 和 dispatch。

将相关逻辑迁移到一个文件当中

这不是必须的,但你可以通过将 reducer 和 context 移动到单个文件中来进一步整理组件。目前,“TasksContext.js” 仅包含两个 context 声明:

import { createContext } from 'react'

export const TasksContext = createContext(null)
export const TasksDispatchContext = createContext(null)

来给这个文件添加更多代码!将 reducer 移动到此文件中,然后声明一个新的 TasksProvider 组件。此组件将所有部分连接在一起:

  • 它将管理 reducer 的状态。
  • 它将提供现有的 context 给组件树。
  • 它将 把 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 组件更加直观:

TasksContext.jsx
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 },
]
你也可以从 TasksContext.js 中导出使用 context 的函数:
export function useTasks() {
return useContext(TasksContext)
}

export function useTasksDispatch() {
return useContext(TasksDispatchContext)
}
组件可以通过以下函数读取 context:
const tasks = useTasks()
const dispatch = useTasksDispatch()
  • 这不会改变任何行为,但它会允许你之后进一步分割这些 context 或向这些函数添加一些逻辑。
  • 现在所有的 context 和 reducer 连接部分都在 TasksContext.js 中。
  • 这保持了组件的干净和整洁,让我们专注于它们显示的内容,而不是它们从哪里获得数据:

  • 你可以将 TasksProvider 视为页面的一部分,它知道如何处理 tasks。useTasks 用来读取它们,useTasksDispatch 用来从组件树下的任何组件更新它们。

注意:

  • 像 useTasks 和 useTasksDispatch 这样的函数被称为 自定义 Hook。
  • 如果你的函数名以 use 开头,它就被认为是一个自定义 Hook。
  • 这让你可以使用其他 Hook,比如 useContext。

  • 随着应用的增长,你可能会有许多这样的 context 和 reducer 的组合。
  • 这是一种强大的拓展应用并 提升状态 的方式,让你在组件树深处访问数据时无需进行太多工作。

摘要

  • 你可以将 reducer 与 context 相结合,让任何组件读取和更新它的状态。

  • 为子组件提供 state 和 dispatch 函数:

    • 创建两个 context (一个用于 state,一个用于 dispatch 函数)。
    • 让组件的 context 使用 reducer。
    • 使用组件中需要读取的 context。
  • 你可以通过将所有传递信息的代码移动到单个文件中来进一步整理组件。

    • 你可以导出一个像 TasksProvider 可以提供 context 的组件。
    • 你也可以导出像 useTasks 和 useTasksDispatch 这样的自定义 Hook。
  • 你可以在你的应用程序中大量使用 context 和 reducer 的组合。