使用 Reducer 和上下文进行扩展

Reducer 让你可以整合组件的状态更新逻辑。上下文使你可以将信息深入传递给其他组件。你可以将 reducer 和上下文结合在一起来管理复杂屏幕的状态。

¥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 与上下文结合起来

    ¥How to combine a reducer with context

  • 如何避免通过属性传递状态和调度

    ¥How to avoid passing state and dispatch through props

  • 如何将上下文和状态逻辑保持在单独的文件中

    ¥How to keep context and state logic in a separate file

结合 reducer 和上下文

¥Combining a reducer with context

reducer 介绍 的这个例子中,状态由 reducer 管理。reducer 函数包含所有状态更新逻辑,并在此文件的底部声明:

¥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:

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 组件中可用。要让其他组件读取任务列表或更改它,你必须显式 传递 当前状态和更改它的事件处理程序作为属性。

¥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.

例如,TaskApp 将任务列表和事件处理程序传递给 TaskList

¥For example, TaskApp passes a list of tasks and the event handlers to TaskList:

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

TaskList 将事件处理程序传递给 Task

¥And TaskList passes the event handlers to 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!

这就是为什么,作为通过属性传递它们的替代方案,你可能希望将 tasks 状态和 dispatch 函数都放在 到上下文中。 中,这样,树中 TaskApp 以下的任何组件都可以读取任务并分派操作,而无需重复 “属性钻取”。

¥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”.

以下是如何将 reducer 与上下文结合起来:

¥Here is how you can combine a reducer with context:

  1. 创建上下文。

    ¥Create the context.

  2. 将状态和调度放入上下文中。

    ¥Put state and dispatch into context.

  3. 在树中的任何位置使用上下文。

    ¥Use context anywhere in the tree.

步骤 1:创建上下文

¥Step 1: Create the context

useReducer 钩子返回当前的 tasksdispatch 函数,让你更新它们:

¥The useReducer Hook returns the current tasks and the dispatch function that lets you update them:

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

要将它们向下传递到树中,你将 创建 两个单独的上下文:

¥To pass them down the tree, you will create two separate contexts:

  • TasksContext 提供当前的任务列表。

    ¥TasksContext provides the current list of tasks.

  • TasksDispatchContext 提供让组件调度操作的函数。

    ¥TasksDispatchContext provides the function that lets components dispatch actions.

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

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

在这里,你将 null 作为默认值传递给两个上下文。实际值将由 TaskApp 组件提供。

¥Here, you’re passing null as the default value to both contexts. The actual values will be provided by the TaskApp component.

步骤 2:将状态和调度放入上下文中

¥Step 2: Put state and dispatch into context

现在你可以在 TaskApp 组件中导入这两个上下文。获取 useReducer() 返回的 tasksdispatch提供它们 给下面的整个树:

¥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:

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

现在,你通过属性和上下文传递信息:

¥For now, you pass the information both via props and in 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.

步骤 3:在树中的任何位置使用上下文

¥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:

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

而是,任何需要任务列表的组件都可以从 TaskContext 中读取它:

¥Instead, any component that needs the task list can read it from the TaskContext:

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

要更新任务列表,任何组件都可以从上下文中读取 dispatch 函数并调用它:

¥To update the task list, any component can read the dispatch function from context and call it:

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 组件传递任何事件处理程序。每个组件读取它需要的上下文:

¥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:

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 管理。但是,通过导入和使用这些上下文,它的 tasksdispatch 现在可用于树中下方的每个组件。

¥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.

将所有线路移动到一个文件中

¥Moving all wiring into a single file

你不必这样做,但你可以通过将 reducer 和上下文移动到一个文件中来进一步整理组件。目前,TasksContext.js 仅包含两个上下文声明:

¥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:

import { createContext } from 'react';

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

这个文件即将变得拥挤!你将把 reducer 移动到同一个文件中。然后你将在同一个文件中声明一个新的 TasksProvider 组件。该组件会将所有部分连接在一起:

¥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:

  1. 它将使用 reducer 管理状态。

    ¥It will manage the state with a reducer.

  2. 它将为下面的组件提供这两种上下文。

    ¥It will provide both contexts to components below.

  3. 它会 children 为属性 所以你可以将 JSX 传递给它。

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

这移除了 TaskApp 组件的所有复杂性和线路:

¥This removes all the complexity and wiring from your TaskApp component:

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 上下文的函数:

¥You can also export functions that use the context from TasksContext.js:

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:

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

这不会以任何方式改变行为,但它允许你稍后进一步拆分这些上下文或向这些函数添加一些逻辑。现在所有上下文和 reducer 接线都在 TasksContext.js 中。这使组件保持干净整洁,专注于它们显示的内容而不是获取数据的位置:

¥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:

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 视为屏幕的一部分,知道如何处理任务,将 useTasks 视为读取任务的方式,将 useTasksDispatch 视为从树中下方的任何组件更新任务的方式。

¥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.

注意

useTasksuseTasksDispatch 这样的函数称为 自定义钩子。 如果它的名称以 use 开头,则你的函数被认为是自定义钩子。这使你可以在其中使用其他钩子,例如 useContext

¥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.

随着你的应用的增长,你可能会有许多这样的上下文-reducer 对。这是一种无需太多工作即可扩展你的应用和 提升状态 的强大方法,无论你何时想要访问树深处的数据。

¥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.

回顾

  • 你可以将 reducer 与上下文结合起来,让任何组件读取和更新其上方的状态。

    ¥You can combine reducer with context to let any component read and update state above it.

  • 为以下组件提供状态和调度函数:

    ¥To provide state and the dispatch function to components below:

    1. 创建两个上下文(用于状态和调度函数)。

      ¥Create two contexts (for state and for dispatch functions).

    2. 从使用 reducer 的组件中提供两个上下文。

      ¥Provide both contexts from the component that uses the reducer.

    3. 使用来自需要读取它们的组件的任一上下文。

      ¥Use either context from components that need to read them.

  • 你可以通过将所有线路移动到一个文件中来进一步整理组件。

    ¥You can further declutter the components by moving all wiring into one file.

    • 你可以导出提供上下文的组件,例如 TasksProvider

      ¥You can export a component like TasksProvider that provides context.

    • 你也可以导出像 useTasksuseTasksDispatch 这样的自定义钩子来读取它。

      ¥You can also export custom Hooks like useTasks and useTasksDispatch to read it.

  • 你的应用中可以有很多这样的上下文-reducer 对。

    ¥You can have many context-reducer pairs like this in your app.


React 中文网 - 粤ICP备13048890号