使用 Reducer 和 Context 扩展应用规模

Reducer 允许你整合组件的状态更新逻辑。Context 允许你将信息深入传递到其他组件。你可以将 Reducer 和 Context 结合起来管理复杂屏幕的状态。

你将学习

  • 如何将 Reducer 与 Context 结合使用
  • 如何避免通过 props 传递状态和 dispatch 函数
  • 如何将 Context 和状态逻辑保存在单独的文件中

将 Reducer 与 Context 结合使用

在这个来自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 }
];

Reducer 有助于保持事件处理程序简短简洁。但是,随着应用程序的增长,你可能会遇到另一个难题。当前,tasks 状态和 dispatch 函数仅在顶级 TaskApp 组件中可用。要让其他组件读取任务列表或更改任务列表,你必须显式地向下传递当前状态和更改它的事件处理程序作为 props。

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

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

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

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

在这个小例子中,这很好用,但是如果你中间有数十个或数百个组件,传递所有状态和函数会非常令人沮丧!

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

以下是如何将 Reducer 与 Context 结合使用

  1. 创建 Context。
  2. 状态和 dispatch 放入 Context 中。
  3. 树中的任何位置使用 Context。

步骤 1:创建 Context

useReducer Hook 返回当前的 tasksdispatch 函数,该函数允许你更新它们

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

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

  • TasksContext 提供当前的任务列表。
  • TasksDispatchContext 提供允许组件分派操作的函数。

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

import { createContext } from 'react';

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

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

步骤 2:将状态和分发器放入上下文

现在你可以将两个上下文都导入到你的 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 和上下文两种方式传递信息。

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

在下一步中,你将移除 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);
// ...

要更新任务列表,任何组件都可以从上下文中读取 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>
// ...

TaskApp 组件不向下传递任何事件处理程序,TaskList 也不向 Task 组件传递任何事件处理程序。每个组件都读取它需要的上下文。

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 现在可以通过导入和使用这些上下文,对树中下面的每个组件都可用。

将所有接线移入单个文件

你不需要这样做,但是你可以通过将 reducer 和上下文都移到一个文件中来进一步简化组件。目前,TasksContext.js 只包含两个上下文声明。

import { createContext } from 'react';

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

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

  1. 它将使用 reducer 管理状态。
  2. 它将把两个上下文都提供给下面的组件。
  3. 它将 以 prop 的形式接受 children,以便你可以向其传递 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 组件中移除所有复杂性和接线。

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 _使用_ 上下文的函数。

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

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

当组件需要读取上下文时,可以通过这些函数来实现。

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

这不会以任何方式改变行为,但它允许你稍后进一步拆分这些上下文或向这些函数添加一些逻辑。现在所有上下文和 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>
  );
}

你可以将 TasksProvider 视为知道如何处理任务的屏幕的一部分,useTasks 作为读取它们的一种方式,以及 useTasksDispatch 作为从树中任何下面的组件更新它们的一种方式。

注意

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

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

回顾

  • 你可以将 reducer 与上下文结合起来,让任何组件读取和更新其上方的状态。
  • 要将状态和分发函数提供给下面的组件
    1. 创建两个上下文(一个用于状态,一个用于分发函数)。
    2. 从使用 reducer 的组件中提供这两个上下文。
    3. 从需要读取它们的组件中使用任一上下文。
  • 你可以通过将所有接线移到一个文件中来进一步简化组件。
    • 你可以导出像 TasksProvider 这样的组件来提供上下文。
    • 您也可以导出自定义 Hook,例如 useTasksuseTasksDispatch 来读取它。
  • 您可以在应用中拥有许多这样的上下文-reducer 对。