react之Reducer和Context的联合使用

第三章 - 状态管理

使用 Reducer 和 Context 拓展你的应用

Reducer 可以整合组件的状态更新逻辑。Context 可以将信息深入传递给其他组件。你可以组合使用它们来共同管理一个复杂页面的状态。

结合使用 reducer 和 context

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

react 复制代码
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 将 一系列 task 和事件处理程序传递给 TaskList

react 复制代码
<TaskList
  tasks={tasks}
  onChangeTask={handleChangeTask}
  onDeleteTask={handleDeleteTask}
/>

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

react 复制代码
<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 返回当前的 tasksdispatch 函数来让你更新它们:

react 复制代码
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

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

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

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

react 复制代码
import { createContext } from 'react';

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

在这里,你把 null 作为默认值传递给两个 context。实际值是由 TaskApp 组件提供的。

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

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

react 复制代码
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>
  );
}

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

现在你不需要将tasks和事件处理程序在组件树中传递:

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

相反,任何需要tasks的组件都可以从TaskContext中读取它:

react 复制代码
export default function TaskList() {
	const tasks = useContext(TasksContext)
}

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

react 复制代码
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:

react 复制代码
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>
  );
}

state 仍然 "存在于" 顶层 Task 组件中,由 useReducer 进行管理 。不过,组件树里的组件只要导入这些 context 之后就可以获取 tasksdispatch

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

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

react 复制代码
import { createContext } from 'react'

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

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

  1. 它将管理 reducer 的状态。
  2. 它将提供现有的 context 给组件树。
  3. 它将 children 作为 prop,所以你可以传递 JSX。
react 复制代码
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 组件更加直观:

react 复制代码
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 中导出使用 context 的函数:

react 复制代码
export function useTasks() {

  return useContext(TasksContext);

}



export function useTasksDispatch() {

  return useContext(TasksDispatchContext);

}

组件可以通过以下函数读取 context:

react 复制代码
const tasks = useTasks();

const dispatch = useTasksDispatch();

这不会改变任何行为,但它会允许你之后进一步分割这些 context 或向这些函数添加一些逻辑。现在所有的 context 和 reducer 连接部分都在 TasksContext.js 中。这保持了组件的干净和整洁,让我们专注于它们显示的内容,而不是它们从哪里获得数据:

react 复制代码
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。useTasks 用来读取它们,useTasksDispatch 用来从组件树下的任何组件更新它们。

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

摘要
  • 你可以将reducer和context结合,让任何组件读取和更新它的状态
  • 为子组件提供state 和 dispatch函数:
    1. 创建两个context(一个用于state,一个用于dispatch函数)
    2. 让组件的context使用reducer
    3. 使用组件中需要读取的context
  • 你可以将所有传递信息的代码移动到单个文件来进一步整理组件
    • 你可以导出一个像 TasksProvider 可以提供 context 的组件。
    • 你也可以导出像 useTasksuseTasksDispatch 这样的自定义 Hook。
  • 你可以在你的应用程序中大量使用context 和 reducer 的组合
相关推荐
kingwebo'sZone2 分钟前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_090121 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农33 分钟前
Vue 2.3
前端·javascript·vue.js
夜郎king1 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳1 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_2 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝2 小时前
RBAC前端架构-01:项目初始化
前端·架构
程序员agions2 小时前
2026年,微前端终于“死“了
前端·状态模式