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

相关文章

Vue和React受控组件的区别小结

《Vue和React受控组件的区别小结》本文主要介绍了Vue和React受控组件的区别小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录背景React 的实现vue3 的实现写法一:直接修改事件参数写法二:通过ref引用 DOMVu

Java实现将HTML文件与字符串转换为图片

《Java实现将HTML文件与字符串转换为图片》在Java开发中,我们经常会遇到将HTML内容转换为图片的需求,本文小编就来和大家详细讲讲如何使用FreeSpire.DocforJava库来实现这一功... 目录前言核心实现:html 转图片完整代码场景 1:转换本地 HTML 文件为图片场景 2:转换 H

C#使用Spire.Doc for .NET实现HTML转Word的高效方案

《C#使用Spire.Docfor.NET实现HTML转Word的高效方案》在Web开发中,HTML内容的生成与处理是高频需求,然而,当用户需要将HTML页面或动态生成的HTML字符串转换为Wor... 目录引言一、html转Word的典型场景与挑战二、用 Spire.Doc 实现 HTML 转 Word1

Vue3绑定props默认值问题

《Vue3绑定props默认值问题》使用Vue3的defineProps配合TypeScript的interface定义props类型,并通过withDefaults设置默认值,使组件能安全访问传入的... 目录前言步骤步骤1:使用 defineProps 定义 Props步骤2:设置默认值总结前言使用T

基于Python Playwright进行前端性能测试的脚本实现

《基于PythonPlaywright进行前端性能测试的脚本实现》在当今Web应用开发中,性能优化是提升用户体验的关键因素之一,本文将介绍如何使用Playwright构建一个自动化性能测试工具,希望... 目录引言工具概述整体架构核心实现解析1. 浏览器初始化2. 性能数据收集3. 资源分析4. 关键性能指

从入门到精通详解LangChain加载HTML内容的全攻略

《从入门到精通详解LangChain加载HTML内容的全攻略》这篇文章主要为大家详细介绍了如何用LangChain优雅地处理HTML内容,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录引言:当大语言模型遇见html一、HTML加载器为什么需要专门的HTML加载器核心加载器对比表二

前端如何通过nginx访问本地端口

《前端如何通过nginx访问本地端口》:本文主要介绍前端如何通过nginx访问本地端口的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、nginx安装1、下载(1)下载地址(2)系统选择(3)版本选择2、安装部署(1)解压(2)配置文件修改(3)启动(4)

HTML中meta标签的常见使用案例(示例详解)

《HTML中meta标签的常见使用案例(示例详解)》HTMLmeta标签用于提供文档元数据,涵盖字符编码、SEO优化、社交媒体集成、移动设备适配、浏览器控制及安全隐私设置,优化页面显示与搜索引擎索引... 目录html中meta标签的常见使用案例一、基础功能二、搜索引擎优化(seo)三、社交媒体集成四、移动

HTML input 标签示例详解

《HTMLinput标签示例详解》input标签主要用于接收用户的输入,随type属性值的不同,变换其具体功能,本文通过实例图文并茂的形式给大家介绍HTMLinput标签,感兴趣的朋友一... 目录通用属性输入框单行文本输入框 text密码输入框 password数字输入框 number电子邮件输入编程框

HTML img标签和超链接标签详细介绍

《HTMLimg标签和超链接标签详细介绍》:本文主要介绍了HTML中img标签的使用,包括src属性(指定图片路径)、相对/绝对路径区别、alt替代文本、title提示、宽高控制及边框设置等,详细内容请阅读本文,希望能对你有所帮助... 目录img 标签src 属性alt 属性title 属性width/h