使用 Reducer 和 Context 拓展你的应用 | Scaling Up with Reducer and Context

Reducers let you consolidate a component’s state update logic. Context lets you pass information deep down to other components. You can combine reducers and context together to manage state of a complex screen. Reducer 可以整合组件的状态更新逻辑。Context 可以将信息深入传递给其他组件。你可以组合使用它们来共同管理一个复杂页面的状态。

你将会学习到

  • How to combine a reducer with context
  • 如何结合使用 reducer 和 context
  • How to avoid passing state and dispatch through props
  • 如何去避免通过 props 传递 state 和 dispatch
  • How to keep context and state logic in a separate file
  • 如何将 context 和状态逻辑保存在一个单独的文件中

结合使用 reducer 和 context | Combining a reducer with context

In this example from the introduction to reducers, the state is managed by a reducer. The reducer function contains all of the state update logic and is declared at the bottom of this file: 在 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 }
];

A reducer helps keep the event handlers short and concise. However, as your app grows, you might run into another difficulty. Currently, the tasks state and the dispatch function are only available in the top-level TaskApp component. To let other components read the list of tasks or change it, you have to explicitly pass down the current state and the event handlers that change it as props. Reducer 有助于保持事件处理程序的简短明了。但随着应用规模越来越庞大,你就可能会遇到别的困难。目前,tasks 状态和 dispatch 函数仅在顶级 TaskApp 组件中可用。要让其他组件读取任务列表或更改它,你必须显式 传递 当前状态和事件处理程序,将其作为 props。

For example, TaskApp passes a list of tasks and the event handlers to TaskList: 例如,TaskApp 将 一系列 task 和事件处理程序传递给 TaskList

<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>

And TaskList passes the event handlers to Task: TaskList 将事件处理程序传递给 Task

<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>

In a small example like this, this works well, but if you have tens or hundreds of components in the middle, passing down all state and functions can be quite frustrating! 在像这样的小示例里这样做没什么问题,但是如果你有成百上千个中间组件,传递所有状态和函数可能会非常麻烦!

This is why, as an alternative to passing them through props, you might want to put both the tasks state and the dispatch function into context. This way, any component below TaskApp in the tree can read the tasks and dispatch actions without the repetitive “prop drilling”. 这就是为什么,比起通过 props 传递它们,你可能想把 tasks 状态和 dispatch 函数都 放入 context这样,所有的在 TaskApp 组件树之下的组件都不必一直往下传 props 而可以直接读取 tasks 和 dispatch 函数

Here is how you can combine a reducer with context: 下面将介绍如何结合使用 reducer 和 context:

  1. Create the context.
  • 创建 context。
  1. Put state and dispatch into context.
  • 将 state 和 dispatch 放入 context。
  1. Use context anywhere in the tree.
  • 在组件树的任何地方 使用 context。

第一步: 创建 context | Step 1: Create the context

The useReducer Hook returns the current tasks and the dispatch function that lets you update them: useReducer 返回当前的 tasksdispatch 函数来让你更新它们:

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

To pass them down the tree, you will create two separate contexts: 为了将它们从组件树往下传,你将 创建 两个不同的 context:

  • TasksContext provides the current list of tasks.
  • TasksContext 提供当前的 tasks 列表。
  • TasksDispatchContext provides the function that lets components dispatch actions.
  • TasksDispatchContext 提供了一个函数可以让组件分发动作。

Export them from a separate file so that you can later import them from other files: 将它们从单独的文件导出,以便以后可以从其他文件导入它们:

import { createContext } from 'react';

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

Here, you’re passing null as the default value to both contexts. The actual values will be provided by the TaskApp component. 在这里,你把 null 作为默认值传递给两个 context。实际值是由 TaskApp 组件提供的。

第二步: 将 state 和 dispatch 函数放入 context | Step 2: Put state and dispatch into context

Now you can import both contexts in your TaskApp component. Take the tasks and dispatch returned by useReducer() and provide them to the entire tree below: 现在,你可以将所有的 context 导入 TaskApp 组件。获取 useReducer() 返回的 tasksdispatch 并将它们 提供 给整个组件树:

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 和 context 传递信息:

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 }
];

In the next step, you will remove prop passing. 在下一步中,你将删除通过 props 传递的代码。

Step 3: 在组件树中的任何地方使用 context | Step 3: Use context anywhere in the tree

Now you don’t need to pass the list of tasks or the event handlers down the tree: 现在你不需要将 tasks 和事件处理程序在组件树中传递:

<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>

Instead, any component that needs the task list can read it from the TaskContext: 相反,任何需要 tasks 的组件都可以从 TaskContext 中读取它:

export default function TaskList() {
const tasks = useContext(TasksContext);
// ...

To update the task list, any component can read the dispatch function from context and call it: 任何组件都可以从 context 中读取 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>
// ...

The TaskApp component does not pass any event handlers down, and the TaskList does not pass any event handlers to the Task component either. Each component reads the context that it needs: TaskApp 组件不会向下传递任何事件处理程序,TaskList 也不会。每个组件都会读取它需要的 context:

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>
  );
}

The state still “lives” in the top-level TaskApp component, managed with useReducer. But its tasks and dispatch are now available to every component below in the tree by importing and using these contexts. state 仍然 “存在于” 顶层 Task 组件中,由 useReducer 进行管理。不过,组件树里的组件只要导入这些 context 之后就可以获取 tasksdispatch

将相关逻辑迁移到一个文件当中 | Moving all wiring into a single file

You don’t have to do this, but you could further declutter the components by moving both reducer and context into a single file. Currently, TasksContext.js contains only two context declarations: 这不是必须的,但你可以通过将 reducer 和 context 移动到单个文件中来进一步整理组件。目前,“TasksContext.js” 仅包含两个 context 声明:

import { createContext } from 'react';

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

This file is about to get crowded! You’ll move the reducer into that same file. Then you’ll declare a new TasksProvider component in the same file. This component will tie all the pieces together: 来给这个文件添加更多代码!将 reducer 移动到此文件中,然后声明一个新的 TasksProvider 组件。此组件将所有部分连接在一起:

  1. It will manage the state with a reducer.
  • 它将管理 reducer 的状态。
  1. It will provide both contexts to components below.
  • 它将提供现有的 context 给组件树。
  1. It will take children as a prop so you can pass JSX to it.
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}

This removes all the complexity and wiring from your TaskApp component: 这将使 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>
  );
}

You can also export functions that use the context from TasksContext.js: 你也可以从 TasksContext.js 中导出使用 context 的函数:

export function useTasks() {
return useContext(TasksContext);
}

export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}

When a component needs to read context, it can do it through these functions: 组件可以通过以下函数读取 context:

const tasks = useTasks();
const dispatch = useTasksDispatch();

This doesn’t change the behavior in any way, but it lets you later split these contexts further or add some logic to these functions. Now all of the context and reducer wiring is in TasksContext.js. This keeps the components clean and uncluttered, focused on what they display rather than where they get the data: 这不会改变任何行为,但它会允许你之后进一步分割这些 context 或向这些函数添加一些逻辑。现在所有的 context 和 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>
  );
}

You can think of TasksProvider as a part of the screen that knows how to deal with tasks, useTasks as a way to read them, and useTasksDispatch as a way to update them from any component below in the tree. 你可以将 TasksProvider 视为页面的一部分,它知道如何处理 tasks。useTasks 用来读取它们,useTasksDispatch 用来从组件树下的任何组件更新它们。

注意

Functions like useTasks and useTasksDispatch are called Custom Hooks. Your function is considered a custom Hook if its name starts with use. This lets you use other Hooks, like useContext, inside it. 像 useTasksuseTasksDispatch 这样的函数被称为 自定义 Hook 如果你的函数名以 use 开头,它就被认为是一个自定义 Hook。这让你可以使用其他 Hook,比如 useContext

As your app grows, you may have many context-reducer pairs like this. This is a powerful way to scale your app and lift state up without too much work whenever you want to access the data deep in the tree. 随着应用的增长,你可能会有许多这样的 context 和 reducer 的组合。这是一种强大的拓展应用并 提升状态 的方式,让你在组件树深处访问数据时无需进行太多工作。

摘要

  • You can combine reducer with context to let any component read and update state above it.
  • 你可以将 reducer 与 context 相结合,让任何组件读取和更新它的状态。
  • To provide state and the dispatch function to components below:
  • 为子组件提供 state 和 dispatch 函数:
    1. Create two contexts (for state and for dispatch functions).
    • 创建两个 context (一个用于 state,一个用于 dispatch 函数)。
    1. Provide both contexts from the component that uses the reducer.
    • 让组件的 context 使用 reducer。
    1. Use either context from components that need to read them.
    • 使用组件中需要读取的 context。
  • You can further declutter the components by moving all wiring into one file.
  • 你可以通过将所有传递信息的代码移动到单个文件中来进一步整理组件。
    • You can export a component like TasksProvider that provides context.
    • 你可以导出一个像 TasksProvider 可以提供 context 的组件。
    • You can also export custom Hooks like useTasks and useTasksDispatch to read it.
    • 你也可以导出像 useTasksuseTasksDispatch 这样的自定义 Hook。
  • You can have many context-reducer pairs like this in your app.
  • 你可以在你的应用程序中大量使用 context 和 reducer 的组合。