重学React(四):状态管理二

背景: 状态管理的内容实在是太多了,看着看着发现一篇写不完,所以又开了一篇,不得不说,还是很有收获的,接下来就继续学习吧~

前期回顾:
重学React(一):描述UI
重学React(二):添加交互
重学React(三):状态管理

学习内容:

React官网教程:https://zh-hans.react.dev/learn/managing-state

其他辅助资料(看到再补充)

补充说明:这次学习更多的是以学习笔记的形式记录,看到哪记到哪

1. 迁移状态逻辑至 Reducer 中

之前都是基于state的状态管理,state能处理绝大多数场景下的问题,但有时候也可以使用一些其他的方式,更好更方便的整合代码,提高代码的易用性和可复用性。Reducer就是官方推荐的一个用于状态更新的函数。

使用 reducer 整合状态逻辑

请先看下面的代码:

js 复制代码
import { useState } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, setTasks] = useState(initialTasks);

  function handleAddTask(text) {
    setTasks([
      ...tasks,
      {
        id: nextId++,
        text: text,
        done: false,
      },
    ]);
  }

  function handleChangeTask(task) {
    setTasks(
      tasks.map((t) => {
        if (t.id === task.id) {
          return task;
        } else {
          return t;
        }
      })
    );
  }

  function handleDeleteTask(taskId) {
    setTasks(tasks.filter((t) => t.id !== taskId));
  }

  return (
    <>
      <h1>布拉格的行程安排</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: '参观卡夫卡博物馆', done: true},
  {id: 1, text: '看木偶戏', done: false},
  {id: 2, text: '打卡列侬墙', done: false},
];

代码很好理解,实现的功能是数组的增加,编辑和删除。这些操作都是通过更新setTasks来进行的。随着操作状态越来越多(可能以后有批量删,批量加等等的需求),状态逻辑也会越来越多,代码维护起来复杂度就会变高,这个时候就适合用Reducer函数重构一下(重构,程序员毕生的追求)。Reducer函数最主要的目的在于统一管理逻辑状态,它是处理状态的另一种形式。单纯说概念可能有点难理解,那就先看如何实现吧。

1. 将设置状态的逻辑修改成dispatch的一个action

使用 reducer 管理状态与直接设置状态略有不同。它不是通过设置状态来告诉 React "要做什么",而是通过事件处理程序 dispatch 一个 "action" 来指明 "用户刚刚做了什么"。(而状态更新逻辑则保存在其他地方!)因此,我们不再通过事件处理器直接 "设置 task",而是 dispatch 一个 "添加/修改/删除任务" 的 action。这更加符合用户的思维。

js 复制代码
// 我们先把这三个处理状态逻辑的方法单独拿出来
function handleAddTask(text) {
  setTasks([
    ...tasks,
    {
      id: nextId++,
      text: text,
      done: false,
    },
  ]);
}

function handleChangeTask(task) {
  setTasks(
    tasks.map((t) => {
      if (t.id === task.id) {
        return task;
      } else {
        return t;
      }
    })
  );
}

function handleDeleteTask(taskId) {
  setTasks(tasks.filter((t) => t.id !== taskId));
}

// 在使用Reducer函数后,多了一个dispatch方法,所以可以将代码改写成这样
function handleAddTask(text) {
  dispatch(
  // 调用了一个叫type是added的action
  // 通常在reducer中会用type来描述发生了什么,比如这个例子type就能体现出来,实现的是添加功能,不一定用这个,但比较通用。
  {
    type: 'added', // 
    id: nextId++,
    text: text,
  });
}

function handleChangeTask(task) {
  dispatch({
    type: 'changed',
    task: task,
  });
}

function handleDeleteTask(taskId) {
  dispatch({
    type: 'deleted',
    id: taskId,
  });
}

2. 编写一个Reducer函数

reducer 函数就是用来放置状态逻辑的地方。它接受两个参数,分别为当前 state 和 action 对象,并且返回的是更新后的 state。

由于 reducer 函数接受 state(tasks)作为参数,因此你可以 在组件之外声明它。这减少了代码的缩进级别,提升了代码的可读性

直接看代码吧。

js 复制代码
// tasks就是当前的state状态,sction就是上面例子中dispatch传入的对象
// 在action中通常通过type来区分操作
// 这段代码可以在组件之外声明,如果有很多组件都有类似的方法,还可以抽出来实现复用
function tasksReducer(tasks, action) {
// 可以用switch/case的地方就能用if/else,但在reducer函数中,switch/case会更加的一目了然
  switch (action.type) {
    case 'added': {
    // reducer返回的是下一次的状态
    // 可以等同于setTasks的内容
      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('未知 action: ' + action.type);
    }
  }
}

3. 在组件中使用Reducer

写好tasksReducer函数后,最后一步就是要将函数导入到组件中,这样才能愉快的使用dispatch函数。

关键点就是useReducer这个hook

它接受 2 个参数:

  1. 一个 reducer 函数
  2. 一个初始的 state

返回如下内容:

  1. 一个有状态的值

  2. 一个 dispatch 函数(用来 "派发" 用户操作给 reducer)

这样写完的好处是,状态逻辑和组件分离,这样能更好的理解组件逻辑。

下面来看代码吧:

js 复制代码
// 引入useReducer hooks
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
// 这个hook返回的tasks是有状态的值,和最开始useState声明的状态是一致的
// dispatch目的是派发用户操作,告诉用户使用的是什么
  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>布拉格的行程安排</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('未知 action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: '参观卡夫卡博物馆', done: true},
  {id: 1, text: '看木偶戏', done: false},
  {id: 2, text: '打卡列侬墙', done: false}
];

对比 useState 和 useReducer

useReducer也不是万能的,它们俩虽然某种程度上可以互换,但各自使用场景还是有所区别的。最简单的控制展示与否的功能,如果用reducer就未免大材小用还增加了冗余代码,下面是比较useState和useReducer的方法。
代码体积: 通常,在使用 useState 时,一开始只需要编写少量代码。而 useReducer 必须提前编写 reducer 函数和需要调度的 actions。但是,当多个事件处理程序以相似的方式修改 state 时,useReducer 可以减少代码量。
可读性: 当状态更新逻辑足够简单时,useState 的可读性还行。但是,一旦逻辑变得复杂起来,它们会使组件变得臃肿且难以阅读。在这种情况下,useReducer 允许你将状态更新逻辑与事件处理程序分离开来。
可调试性: 当使用 useState 出现问题时, 你很难发现具体原因以及为什么。 而使用 useReducer 时, 你可以在 reducer 函数中通过打印日志的方式来观察每个状态的更新,以及为什么要更新(来自哪个 action)。 如果所有 action 都没问题,你就知道问题出在了 reducer 本身的逻辑中。 然而,与使用 useState 相比,你必须单步执行更多的代码。
可测试性: reducer 是一个不依赖于组件的纯函数。这就意味着你可以单独对它进行测试。一般来说,我们最好是在真实环境中测试组件,但对于复杂的状态更新逻辑,针对特定的初始状态和 action,断言 reducer 返回的特定状态会很有帮助。
个人偏好: 并不是所有人都喜欢用 reducer,没关系,这是个人偏好问题。你可以随时在 useState 和 useReducer 之间切换,它们能做的事情是一样的!

编写一个好的reducer

编写 reducer 时最好牢记以下两点:
reducer 必须是纯粹的。 这一点和 状态更新函数 是相似的,reducer 是在渲染时运行的!(actions 会排队直到下一次渲染)。 这就意味着 reducer 必须纯净,即当输入相同时,输出也是相同的。它们不应该包含异步请求、定时器或者任何副作用(对组件外部有影响的操作)。它们应该以不可变值的方式去更新 对象 和 数组。
每个 action 都描述了一个单一的用户交互,即使它会引发数据的多个变化。 举个例子,如果用户在一个由 reducer 管理的表单(包含五个表单项)中点击了 重置按钮,那么 dispatch 一个 reset_form 的 action 比 dispatch 五个单独的 set_field 的 action 更加合理。如果你在一个 reducer 中打印了所有的 action 日志,那么这个日志应该是很清晰的,它能让你以某种步骤复现已发生的交互或响应。这对代码调试很有帮助!

使用 Immer 简化 reducer

跟之前在对象和数组中使用一样,我们也可以使用Immer来简化reducer的过程,只需要将hooks从useReducer改成useImmerReducer 就好。

js 复制代码
// 这是修改之后的代码,用了Immer就能愉快的肆无忌惮使用数组方法了
import { useImmerReducer } from 'use-immer';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

function tasksReducer(draft, action) {
  switch (action.type) {
    case 'added': {
      draft.push({
        id: action.id,
        text: action.text,
        done: false,
      });
      break;
    }
    case 'changed': {
      const index = draft.findIndex((t) => t.id === action.task.id);
      draft[index] = action.task;
      break;
    }
    case 'deleted': {
      return draft.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('未知 action:' + action.type);
    }
  }
}

export default function TaskApp() {
  const [tasks, dispatch] = useImmerReducer(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>布拉格的行程安排</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: '参观卡夫卡博物馆', done: true},
  {id: 1, text: '看木偶戏', done: false},
  {id: 2, text: '打卡列侬墙', done: false},
];

为什么叫reducer函数

尽管 reducer 可以 "减少" 组件内的代码量,但它实际上是以数组上的 reduce() 方法命名的。

reduce() 允许将数组中的多个值 "累加" 成一个值

传递给 reduce 的函数被称为 "reducer"。它接受 目前的结果 和 当前的值,然后返回 下一个结果。React 中的 reducer 和这个是一样的:它们都接受 目前的状态 和 action ,然后返回 下一个状态。这样,action 会随着时间推移累积到状态中。

使用 Context 深层传递参数

状态管理中有一个很常用的方法:状态提升,它通常用于需要统一控制子组件状态的场景。但是当需要props深层传递参数的场景时,一层层的传递props会变得很繁琐,而且万一遗漏掉某一层没有传,叶子结点就不会生效了。想象一个主题切换的场景,暗黑模式和明亮模式的切换应该是全局生效的,所以我们需要将这个设置尽可能放到顶层。所有涉及样式的组件都需要知道当前的设置是暗黑模式还是明亮模式。按照之前的逻辑,这个属性需要透传到所有涉及的组件。想想都觉得麻烦。如果可以一步到位,只需要在使用这个功能的后代节点中引用,那就最好不过了。

React很愉快的提供了这个功能,也就是Context。Context 可以让父节点,甚至是很远的父节点都可以为其内部的整个组件树提供数据。

接下来通过官方例子看看Context要如何使用:

js 复制代码
// Heading.js
export default function Heading({ level, children }) {
  switch (level) {
    case 1:
      return <h1>{children}</h1>;
    case 2:
      return <h2>{children}</h2>;
    case 3:
      return <h3>{children}</h3>;
    case 4:
      return <h4>{children}</h4>;
    case 5:
      return <h5>{children}</h5>;
    case 6:
      return <h6>{children}</h6>;
    default:
      throw Error('未知的 level:' + level);
  }
}
js 复制代码
// Section.js
export default function Section({ children }) {
  return (
    <section className="section">
      {children}
    </section>
  );
}
js 复制代码
// App.js
import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading level={1}>主标题</Heading>
      <Heading level={2}>副标题</Heading>
      <Heading level={3}>子标题</Heading>
      <Heading level={4}>子子标题</Heading>
      <Heading level={5}>子子子标题</Heading>
      <Heading level={6}>子子子子标题</Heading>
    </Section>
  );
}

假设想让相同 Section 中的多个 Heading 具有相同的尺寸,代码可以写成这样:

js 复制代码
import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading level={1}>主标题</Heading>
      <Section>
        <Heading level={2}>副标题</Heading>
        <Heading level={2}>副标题</Heading>
        <Heading level={2}>副标题</Heading>
        <Section>
          <Heading level={3}>子标题</Heading>
          <Heading level={3}>子标题</Heading>
          <Heading level={3}>子标题</Heading>
          <Section>
            <Heading level={4}>子子标题</Heading>
            <Heading level={4}>子子标题</Heading>
            <Heading level={4}>子子标题</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

这个代码看上去是不是有点冗余,我们的目的是相同section中的Heading level保持一致,假设可以写成这样,代码量会少点,也会清晰些。

js 复制代码
// 省略了其他不必要的代码
<Section level={3}>
  <Heading>关于</Heading>
  <Heading>照片</Heading>
  <Heading>视频</Heading>
</Section>

但是 Heading组件是如何知道离它最近的 Section的 level 的呢?这需要子组件可以通过某种方式"访问"到组件树中某处在其上层的数据。也就是Context发挥作用的时候了。

接下来可以通过三个步骤来实现Context

  1. 创建 一个 context。(你可以将其命名为 LevelContext, 因为它表示的是标题级别。)
  2. 在需要数据的组件内 使用 刚刚创建的 context。(Heading 将会使用 LevelContext。)
  3. 在指定数据的组件中 提供 这个 context。(Section 将会提供 LevelContext。)

1. 创建一个Context

创建这个 context,并 将其从一个文件中导出,这样组件才可以使用它

js 复制代码
import { createContext } from 'react';
// createContext 只需默认值这么一个参数
// 但是可以传递任何类型的参数,包括对象
export const LevelContext = createContext(1);

2. 使用Context

从 React 中引入 useContext Hook 以及刚刚创建的 context,在需要知道Level参数的地方获取context的值,这个例子中需要在Heading中引入

js 复制代码
// Heading.js 
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Heading({ children }) {
// 这个level获取的就是LevelContext中的值
  const level = useContext(LevelContext);
  switch (level) {
    case 1:
      return <h1>{children}</h1>;
    case 2:
      return <h2>{children}</h2>;
    case 3:
      return <h3>{children}</h3>;
    case 4:
      return <h4>{children}</h4>;
    case 5:
      return <h5>{children}</h5>;
    case 6:
      return <h6>{children}</h6>;
    default:
      throw Error('未知的 level:' + level);
  }
}
js 复制代码
// App.js
import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section level={1}>
      <Heading>主标题</Heading>
      <Section level={2}>
        <Heading>副标题</Heading>
        <Heading>副标题</Heading>
        <Heading>副标题</Heading>
        <Section level={3}>
          <Heading>子标题</Heading>
          <Heading>子标题</Heading>
          <Heading>子标题</Heading>
          <Section level={4}>
            <Heading>子子标题</Heading>
            <Heading>子子标题</Heading>
            <Heading>子子标题</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

到这里可能会发现,代码运行之后,所有的Heading level都是1,这是因为我们只是使用Context,但还没提供它,React不知道从哪里去拿Context。但是很神奇的发现,这段代码也不会报错,因为在创建Context时提供了默认值,如果React找不到提供Context的组件,React会使用默认值渲染。

3. 提供Context

知道问题所在,下一步就是在适合的地方提供Context,从App.js中可以看到,level 是通过Section传递的,所以需要对Section组件进行改造

js 复制代码
// Section.js
import { LevelContext } from './LevelContext.js';

export default function Section({ level, children }) {
  return (
    <section className="section">
    // 使用LevelContext包裹之后,children中所有的内容都能获得value中的值
    // 在Section组件中的任何子组件请求 LevelContext,给他们这个 level
      <LevelContext value={level}>
        {children}
      </LevelContext>
    </section>
  );
}

再次观察一下这个需求会发现,App.js中,Section是嵌套渲染,并且每嵌套一次,level层级+1,在这种场景下,我们可以将Section.js修改成这样:

js 复制代码
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Section({ children }) {
// 每个 Section 都会从上层的 Section 读取 level,并自动向下层传递 level + 1
  const level = useContext(LevelContext);
  return (
    <section className="section">
      <LevelContext value={level + 1}>
        {children}
      </LevelContext>
    </section>
  );
}

这样就不需要手动传递Level的值,App.js可以修改成这样:

js 复制代码
import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading>主标题</Heading>
      <Section>
        <Heading>副标题</Heading>
        <Heading>副标题</Heading>
        <Heading>副标题</Heading>
        <Section>
          <Heading>子标题</Heading>
          <Heading>子标题</Heading>
          <Heading>子标题</Heading>
          <Section>
            <Heading>子子标题</Heading>
            <Heading>子子标题</Heading>
            <Heading>子子标题</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

Context 会穿过中间层级的组件

在提供 context 的组件和使用它的组件之间的层级可以插入任意数量的组件。包括像div这样的内置组件和自己创建的组件。

js 复制代码
// 在这个例子里,不需要做任何操作,就能渲染出不同的level
//Section为它内部的树指定一个 context,因此可以在任何地方插入 Heading
import Heading from './Heading.js';
import Section from './Section.js';

export default function ProfilePage() {
  return (
    <Section>
      <Heading>My Profile</Heading>
      <Post
        title="旅行者,你好!"
        body="来看看我的冒险。"
      />
      <AllPosts />
    </Section>
  );
}

function AllPosts() {
  return (
    <Section>
      <Heading>帖子</Heading>
      <RecentPosts />
    </Section>
  );
}

function RecentPosts() {
  return (
    <Section>
      <Heading>最近的帖子</Heading>
      <Post
        title="里斯本的味道"
        body="...那些蛋挞!"
      />
      <Post
        title="探戈节奏中的布宜诺斯艾利斯"
        body="我爱它!"
      />
    </Section>
  );
}

function Post({ title, body }) {
  return (
    <Section isFancy={true}>
      <Heading>
        {title}
      </Heading>
      <p><i>{body}</i></p>
    </Section>
  );
}

Context毕竟覆盖组件范围比较大,如果需要覆盖Context的值,唯一方法是将子组件包裹到一个提供不同值的 context provider 中。

context provider就是我们创建的那个context,因为是提供者,所以通常用provider进行命名

不同的 React context 不会覆盖彼此。通过 createContext() 创建的每个 context 都和其他 context 完全分离,只有使用和提供 那个特定的 context 的组件才会联系在一起。一个组件可以轻松地使用或者提供许多不同的 context。

Context替代方案

Context看起来使用很方便,但也很容易被滥用。当Context满天飞时,使用起来也会变得麻烦。比如props只需要在父子组件中传递的场景下,Context就没有太大的用武之地了。

在使用Context之前,请先考虑以下几种替代方案:

  1. 从传递Props开始。 如果你的组件看起来不起眼,那么通过十几个组件向下传递一堆 props 并不罕见。这有点像是在埋头苦干,但是这样做可以让哪些组件用了哪些数据变得十分清晰!维护你代码的人会很高兴你用 props 让数据流变得更加清晰。
  2. 抽象组件并 将 JSX 作为 children 传递 给它们。 如果你通过很多层不使用该数据的中间组件(并且只会向下传递)来传递数据,这通常意味着你在此过程中忘记了抽象组件。举个例子,你可能想传递一些像 posts 的数据 props 到不会直接使用这个参数的组件,类似 <Layout posts={posts} />。取而代之的是,让 Layout 把 children 当做一个参数,然后渲染 。这样就减少了定义数据的组件和使用数据的组件之间的层级。

Context 的使用场景

  1. 主题: 如果你的应用允许用户更改其外观(例如暗夜模式),你可以在应用顶层放一个 context provider,并在需要调整其外观的组件中使用该 context。
  2. 当前账户: 许多组件可能需要知道当前登录的用户信息。将它放到 context 中可以方便地在树中的任何位置读取它。某些应用还允许你同时操作多个账户(例如,以不同用户的身份发表评论)。在这些情况下,将 UI 的一部分包裹到具有不同账户数据的 provider 中会很方便。
  3. 路由: 大多数路由解决方案在其内部使用 context 来保存当前路由。这就是每个链接"知道"它是否处于活动状态的方式。如果你创建自己的路由库,你可能也会这么做。
  4. 状态管理: 随着你的应用的增长,最终在靠近应用顶部的位置可能会有很多 state。许多遥远的下层组件可能想要修改它们。通常 将 reducer 与 context 搭配使用来管理复杂的状态并将其传递给深层的组件来避免过多的麻烦。
    Context 不局限于静态值。如果下一次渲染时传递不同的值,React 将会更新读取它的所有下层组件!这就是 context 经常和 state 结合使用的原因。

一般而言,如果树中不同部分的远距离组件需要某些信息,context 将会对你大有帮助。

使用 Reducer 和 Context 拓展你的应用

之前说过了Reducer和Context的用处,他们俩其实是state和props的高级用法,结合起来,会产生更多不一样的化学反应,能更好的管理复杂的带状态的组件。

现在回想一下在reducer中举的那个表单例子:

js 复制代码
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。这是不是就联系到了Context?在这个例子中,可以把 tasks 状态和 dispatch 函数都 放入 context。这样,所有的在 TaskApp 组件树之下的组件都不必一直往下传 props 而可以直接读取 tasks 和 dispatch 函数。

结合reducer和context有以下几个步骤
1. 创建 context

js 复制代码
import { createContext } from 'react';
// 创建两个context,分别保存task和dispatch
// 这里默认值是null,因为真实的值需要从App.js中获取(就是reducer声明中)
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

2. 将 state 和 dispatch 函数放入 context

把创建的context导入TaskApp组件中,分别把tasks, dispatch作为参数,这样在TaskApp中的所有子组件都可以直接使用tasks和dispatch

js 复制代码
import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
  // ...
  return (
    <TasksContext value={tasks}>
      <TasksDispatchContext value={dispatch}>
        ...
      </TasksDispatchContext>
    </TasksContext>
  );
}

3. 在组件树中的任何地方使用 context

接下来就可以把要用到task和dispatch的地方都替换一下

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

  return (
    <TasksContext value={tasks}>
      <TasksDispatchContext value={dispatch}>
        <h1>Day off in Kyoto</h1>
        // 这里就不再
        <AddTask />
        <TaskList />
      </TasksDispatchContext>
    </TasksContext>
  );
}
js 复制代码
// AddTask
import { useState, useContext } from 'react';
import { TasksDispatchContext } from './TasksContext.js';

export default function AddTask() {
  const [text, setText] = useState('');
  const dispatch = useContext(TasksDispatchContext);
  return (
    <>
      <input
        placeholder="Add task"
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button onClick={() => {
      // add方法从父组件传入变成了直接dispatch引用,减少了props
        setText('');
        dispatch({
          type: 'added',
          id: nextId++,
          text: text,
        }); 
      }}>Add</button>
    </>
  );
}

let nextId = 3;

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

这一步不是必须的,但可以通过将 reducer 和 context 移动到单个文件中来进一步整理组件。

js 复制代码
// TasksContext.js
// 将reducer的相关逻辑也一并写入,方便管理
import { createContext, useReducer } from 'react';

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

export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

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

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

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

// 甚至可以将tasks和dispatch的引用写成这种形式
// 这种以use开头的函数被称为自定义hook,在自定义hook中可以灵活的使用其他的hook,比如useContext,useState等
export function useTasks() {
  return useContext(TasksContext);
}

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

这样App.js就会变得更加清爽

js 复制代码
// App.js
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>
  );
}

总的来说,reducer和context使用的前提是对state状态管理的深入了解,它们的存在会使得组件封装更好,代码可复用性更强。但有时也要谨慎使用,滥用reducer和context会降低代码可读性,增大维护成本。总之,可以尝试着在应用程序中大量使用 context 和 reducer 的组合。