第二章基础知识:2.5 useReducer 基础知识

132 阅读8分钟

本专栏致力于每周分享一个项目,如果本文对你有帮助的话,欢迎点赞或者关注☘️

React18 源码系列会随着学习 React 源码的实时进度而实时更新:约,两天一小改,五天一大改。

具有分布在多个事件处理程序中的许多状态更新的组件可能会让人不知所措。对于这些情况,您可以将组件外部的所有状态更新逻辑合并到一个函数中,称为“ reducer”。

使用reducer整合状态逻辑

随着组件复杂性的增加,一目了然地了解组件状态更新的所有不同方式会变得越来越困难。例如,下面的TaskApp组件保存一组处于状态的tasks ,并使用三个不同的事件处理程序来添加、删除和编辑任务:

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>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

它的每个事件处理程序都会调用setTasks来更新状态。随着该组件的增长,遍布其中的状态逻辑数量也会随之增加。为了降低这种复杂性并将所有逻辑保存在一个易于访问的位置,您可以将该状态逻辑移至组件外部的单个函数中,称为“reducer”。

什么是Reducer

reducer是处理状态的一种不同方式。您可以通过三个步骤从useState迁移到useReducer

从设置状态(set state)转移到调度动作(dsipatch action)

您的事件处理程序当前通过设置状态来指定要执行的操作

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

删除所有状态设置逻辑。剩下的是三个事件处理程序:

  • 当用户按下“添加”时,将调用handleAddTask(text)
  • 当用户切换任务或按“保存”时,将调用handleChangeTask(task)
  • 当用户按下“Delete”时,将调用handleDeleteTask(taskId)

使用reducer管理状态与直接设置状态略有不同。您不是通过设置状态告诉 React“做什么”,而是通过从事件处理程序分派“操作”来指定“用户刚刚做了什么”。 (状态更新逻辑将位于其他地方!)因此,您不是通过事件处理程序“设置tasks ”,而是调度(dispatch)“添加/更改/删除任务”操作(action)。这更能描述用户的意图。

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

您传递给dispatch对象称为“action”:它是一个常规的 JavaScript 对象。您可以决定在其中放入什么内容,但通常它应该包含有关所发生事件的最少信息.按照惯例,通常给它一个字符串type来描述发生的情况,并在其他字段中传递任何附加信息.

编写一个reducer函数

reducer函数是放置状态逻辑的地方。它接受两个参数,当前状态和操作对象,并返回下一个状态

function yourReducer(state, action) {
  // return next state for React to set
}

React 会将状态设置为您从reducer返回的状态

在此示例中,要将状态设置逻辑从事件处理程序移动到reducer函数,您将:

  • 将当前状态( tasks )声明为第一个参数
  • action对象声明为第二个参数
  • 从reducer返回下一个状态(React 会将状态设置为该状态)。

以下是迁移到reducer函数的所有状态设置逻辑:

function tasksReducer(tasks, action) {
  if (action.type === 'added') {
    return [
      ...tasks,
      {
        id: action.id,
        text: action.text,
        done: false,
      },
    ];
  } else if (action.type === 'changed') {
    return tasks.map((t) => {
      if (t.id === action.task.id) {
        return action.task;
      } else {
        return t;
      }
    });
  } else if (action.type === 'deleted') {
    return tasks.filter((t) => t.id !== action.id);
  } else {
    throw Error('Unknown action: ' + action.type);
  }
}

由于reducer函数将状态( tasks )作为参数,因此您可以在组件外部声明它。 这会减少缩进级别,并使代码更易于阅读。

使用组件中的reducer

最后,您需要将tasksReducer连接到您的组件。从 React 导入useReducer Hook

import { useReducer } from 'react';

然后你可以替换useState

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

useReducer Hook 与useState类似 - 您必须向它传递一个初始状态,它返回一个有状态值和设置状态的方法(在本例中为调度函数)。但有点不同。

useReducer Hook 有两个参数:

  • reducer function
  • initial state

返回:

  • stateful value
  • A dispatch function
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>Prague itinerary</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: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

当您像这样分离关注点时,组件逻辑会更容易阅读。现在,事件处理程序仅通过分派操作来指定发生的情况,而reducer函数则确定状态如何更新以响应它们。

比较useState和useReducer

reducer并非没有缺点!您可以通过以下几种方法来比较它们:

代码大小: 通常,使用useState您必须预先编写更少的代码。使用useReducer ,您必须编写减速器函数调度操作。但是,如果许多事件处理程序以类似的方式修改状态,则useReducer可以帮助减少代码

可读性: 当状态更新很简单时, useState非常容易阅读。当它们变得更加复杂时,它们会使组件的代码变得臃肿并且难以扫描。在这种情况下, useReducer可以让您将更新逻辑的方式与事件处理程序发生的情况清楚地分开。

调试:useState出现错误时,可能很难判断状态设置不正确的位置以及原因。使用useReducer ,您可以将控制台日志添加到您的减速器中,以查看每个状态更新以及发生的原因(由于哪个action )。如果每个action都是正确的,您就会知道错误在于减速器逻辑本身。但是,与useState相比,您必须单步执行更多代码。

测试: reducer 是一个纯函数,不依赖于您的组件。这意味着您可以单独导出和测试它。

如果您经常遇到由于某些组件中不正确的状态更新而导致的错误,并且希望为其代码引入更多结构,我们建议您使用reducer。您不必对所有事情都使用reducer:随意混合和匹配!您甚至可以在同一个组件中使用useStateuseReducer

写好reducer

reducer必须是纯净的。状态更新器功能类似,reducer在渲染期间运行(操作会排队直到下一次渲染。)这意味着reducer必须是纯的——相同的输入总是会产生相同的输出。它们不应发送请求、安排超时或执行任何副作用(影响组件外部事物的操作)。他们应该更新对象数组而不发生突变(mutaion)。

每个操作都描述一次用户交互,即使这会导致数据发生多次更改。 例如,如果用户在具有由reducer管理的五个字段的表单上按“重置”,则分派一个reset_form操作比分派五个单独的set_field操作更有意义。

使用 Immer 编写简洁的reducer

就像在常规状态下更新对象数组一样,您可以使用 Immer 库使reducer更加简洁。在这里, [useImmerReducer](https://github.com/immerjs/use-immer#useimmerreducer)允许您通过pusharr[i] =赋值来改变状态:

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}
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('Unknown 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>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

reducer必须是纯粹的,因此它们不应该改变(mutate)state。但 Immer 为您提供了一个可以安全变异(mutate)的特殊draft对象。在后台,Immer 将使用您对draft所做的更改创建您的状态副本。这就是为什么useImmerReducer管理的减速器可以改变它们的第一个参数并且不需要返回状态。

参考链接

关于作者

作者:Wandra

内容:算法 | 趋势 |源码|Vue | React | CSS | Typescript | Webpack | Vite | GithubAction | GraphQL | Uniqpp。

专栏:欢迎关注呀🌹

本专栏致力于每周分享一个项目,如果本文对你有帮助的话,欢迎点赞或者关注☘️