react之reducers

2024-05-11 01:52

本文主要是介绍react之reducers,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

第三章 - 状态管理

迁移状态逻辑至 Reducer 中

对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序可能会令人不知所措。对于这种情况,你可以将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫做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>布拉格的行程安排</h1><AddTask onAddTask={handleAddTask} /><TaskListtasks={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 是处理状态的另一种方式。你可以通过三个步骤将 useState 迁移到 useReducer

  1. 将设置状态的逻辑修改成 dispatch 的一个action
  2. 编写一个reducer函数
  3. 在你的组件中使用reducer
第一步:将设置状态的逻辑修改成dispatch的一个action

移除所有的状态设置逻辑。只留下三个事件处理函数:

  • handleAddTask(text) 在用户点击 “添加” 时被调用。
  • handleChangeTask(task) 在用户切换任务或点击 “保存” 时被调用。
  • handleDeleteTask(taskId) 在用户点击 “删除” 时被调用。

使用reducer 管理状态与直接设置状态略有不同。他不是通过设置状态来告诉 React 要做什么,而是通过事件处理程序 dispatch一个"action" 来指明 “用户刚刚做了什么”。(而状态更新逻辑则保存在其他地方 !)因此,我们不再通过事件处理器直接"设置task",而是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”

function handleDeleteTask(taskId) {dispatch(// "action" 对象:{type: 'deleted',id: taskId,});
}

他就是一个普通的JavaScript对象。它的结构由你来决定,但通常来说,他应该至少包含可以表明发生了什么事情的信息。(在后面的步骤中,你将会学习如何添加一个 dispatch 函数。)

注意

action 对象可以有多种结构。

按照惯例,我们通常会添加一个字符串类型的 type 字段来描述发生了什么,并通过其它字段传递额外的信息。type 是特定于组件的,在这个例子中 addedaddded_task 都可以。选一个能描述清楚发生的事件的名字!

dispatch({// 针对特定的组件type: 'what_happened',// 其它字段放这里});
第二步:编写一个reducer 函数

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

function yourReducer(state, action) {// 给 React 返回更新后的状态
}

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

在这个例子中,要将状态设置逻辑从事件处理程序移动到reducer函数中,你需要:

  1. 声明当前状态(tasks) 作为第一个参数
  2. 声明action对象作为第二个参数
  3. 从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.id) {return action.task;}else{return t;}})}else if (action.type === 'deleted') {return tasks.filter((t) => t.id !== action.id)}else {throw Error ('未知 action:' + action.type)}
}

由于reducer函数接受state (tasks) 作为参数,因此你可以在组件之外声明它。这减少了代码的缩进级别,提升了代码的可读性。(即可以将庞大的reducer函数单独提取成一个文件,与组件分开,提升代码阅读性)

上面的代码使用了 if/else 语句,但是在 reducers 中使用 switch 语句 是一种惯例。两种方式结果是相同的,但 switch 语句读起来一目了然。我们建议将每个 case 块包装到 {} 花括号中,这样在不同 case 中声明的变量就不会互相冲突。此外,case 通常应该以 return 结尾。如果你忘了 return,代码就会 进入 到下一个 case,这就会导致错误

第三步:在组件中使用reducer

最后,你需要将 tasksReducer导入到组件中。记得先从react中导入 useReducer Hook:

import { useReducer } from 'react';

接下来你就能替换掉之前的useState:

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

useReducer 和 useState很相似 – 你必须传递给他一个初始状态,他会返回一个有状态的值和一个设置该状态的函数(在这个例子中就是dispatch函数)。但是,他们两个之间还是有差异的。

  • useReducer 钩子接受2个参数

    1. 一个初始的reducer函数
    2. 一个初始的state
  • 它返回如下内容:

    1. 一个有状态的值
    2. 一个dispatch函数 (用来派发用户操作给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>布拉格的行程安排</h1><AddTask onAddTask={handleAddTask} /><TaskListtasks={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}
];

如果有需要,你甚至可以把reducer移到一个单独的文件中。当像这样分离关注点时,我们可以更容易地理解组件逻辑。现在,事件处理程序只通过派发 action 来指定 发生了什么,而 reducer 函数通过响应 actions 来决定 状态如何更新

对比 useState 和 useReducer

Reducer并非没有缺点,以下是比较它们的几种方法:

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

如果你在修改某些组件状态时经常出现问题或者想给组件添加更多逻辑时,我们建议你还是使用 reducer。当然,你也不必整个项目都用 reducer,这是可以自由搭配的。你甚至可以在一个组件中同时使用 useStateuseReducer

编写一个好的reducers

编写 reducers 时最好牢记以下两点:

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

使用 Immer 简化 reducers

与在平常的 state 中 修改对象 和 数组 一样,你可以使用 Immer 这个库来简化 reducer。在这里,useImmerReducer 让你可以通过 pusharr[i] = 来修改 state :

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} /><TaskListtasks={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},
];

Reducers应该是纯净的,所以它们不应该去修改state。而Immer为你提供了一种特殊的 draft 对象,你可以通过它安全的修改state。在底层,Immer 会基于当前state 创建一个副本。这就是为什么 useImmerReducer 来管理 reducers 时,可以直接修改第一个参数,且不需要返回一个新的state

摘要
  • 把 useState 转换成 useReducer:
    • 通过事件处理函数 dispatch actions;
    • 编写一个reducer函数,他接受传入的state和一个action,并返回一个新的state;
    • 使用useReducer 替换 useState
  • Reducers 可能需要你写更多的代码,但这有利于代码的调试和测试
  • Reducers必须是纯净的
  • 每个action 都描述了一个单一的用户交互
  • 使用 Immer 来帮助你在reducer里直接修改状态

这篇关于react之reducers的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/978175

相关文章

CSS3 布局样式及其应用举例

《CSS3布局样式及其应用举例》CSS3的布局特性为前端开发者提供了无限可能,无论是Flexbox的一维布局还是Grid的二维布局,它们都能够帮助开发者以更清晰、简洁的方式实现复杂的网页布局,本文给... 目录深入探讨 css3 布局样式及其应用引言一、CSS布局的历史与发展1.1 早期布局的局限性1.2

使用animation.css库快速实现CSS3旋转动画效果

《使用animation.css库快速实现CSS3旋转动画效果》随着Web技术的不断发展,动画效果已经成为了网页设计中不可或缺的一部分,本文将深入探讨animation.css的工作原理,如何使用以及... 目录1. css3动画技术简介2. animation.css库介绍2.1 animation.cs

CSS引入方式和选择符的讲解和运用小结

《CSS引入方式和选择符的讲解和运用小结》CSS即层叠样式表,是一种用于描述网页文档(如HTML或XML)外观和格式的样式表语言,它主要用于将网页内容的呈现(外观)和结构(内容)分离,从而实现... 目录一、前言二、css 是什么三、CSS 引入方式1、行内样式2、内部样式表3、链入外部样式表四、CSS 选

使用雪花算法产生id导致前端精度缺失问题解决方案

《使用雪花算法产生id导致前端精度缺失问题解决方案》雪花算法由Twitter提出,设计目的是生成唯一的、递增的ID,下面:本文主要介绍使用雪花算法产生id导致前端精度缺失问题的解决方案,文中通过代... 目录一、问题根源二、解决方案1. 全局配置Jackson序列化规则2. 实体类必须使用Long封装类3.

Nginx部署React项目时重定向循环问题的解决方案

《Nginx部署React项目时重定向循环问题的解决方案》Nginx在处理React项目请求时出现重定向循环,通常是由于`try_files`配置错误或`root`路径配置不当导致的,本文给大家详细介... 目录问题原因1. try_files 配置错误2. root 路径错误解决方法1. 检查 try_f

在React聊天应用中实现图片上传功能

《在React聊天应用中实现图片上传功能》在现代聊天应用中,除了文字和表情,图片分享也是一个重要的功能,本文将详细介绍如何在基于React的聊天应用中实现图片上传和预览功能,感兴趣的小伙伴跟着小编一起... 目录技术栈实现步骤1. 消息组件改造2. 图片预览组件3. 聊天输入组件改造功能特点使用说明注意事项

一文详解如何在Vue3中封装API请求

《一文详解如何在Vue3中封装API请求》在现代前端开发中,API请求是不可避免的一部分,尤其是与后端交互时,下面我们来看看如何在Vue3项目中封装API请求,让你在实现功能时更加高效吧... 目录为什么要封装API请求1. vue 3项目结构2. 安装axIOS3. 创建API封装模块4. 封装API请求

全解析CSS Grid 的 auto-fill 和 auto-fit 内容自适应

《全解析CSSGrid的auto-fill和auto-fit内容自适应》:本文主要介绍了全解析CSSGrid的auto-fill和auto-fit内容自适应的相关资料,详细内容请阅读本文,希望能对你有所帮助... css  Grid 的 auto-fill 和 auto-fit/* 父元素 */.gri

HTML5中的Microdata与历史记录管理详解

《HTML5中的Microdata与历史记录管理详解》Microdata作为HTML5新增的一个特性,它允许开发者在HTML文档中添加更多的语义信息,以便于搜索引擎和浏览器更好地理解页面内容,本文将探... 目录html5中的Mijscrodata与历史记录管理背景简介html5中的Microdata使用M

html5的响应式布局的方法示例详解

《html5的响应式布局的方法示例详解》:本文主要介绍了HTML5中使用媒体查询和Flexbox进行响应式布局的方法,简要介绍了CSSGrid布局的基础知识和如何实现自动换行的网格布局,详细内容请阅读本文,希望能对你有所帮助... 一 使用媒体查询响应式布局        使用的参数@media这是常用的