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 的组合
相关推荐
汪子熙12 分钟前
Angular 服务器端应用 ng-state tag 的作用介绍
前端·javascript·angular.js
Envyᥫᩣ21 分钟前
《ASP.NET Web Forms 实现视频点赞功能的完整示例》
前端·asp.net·音视频·视频点赞
Мартин.4 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。6 小时前
案例-表白墙简单实现
前端·javascript·css
数云界6 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd6 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常6 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer6 小时前
Vite:为什么选 Vite
前端
小御姐@stella6 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing6 小时前
【React】增量传输与渲染
前端·javascript·面试