React 18 迁移状态逻辑至 Reducer 中
参考文章
迁移状态逻辑至 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
:
- 将设置状态的逻辑 修改 成 dispatch 的一个 action;
- 编写 一个 reducer 函数;
- 在组件中 使用 reducer。
第 1 步: 将设置状态的逻辑修改成 dispatch 的一个 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)
在用户切换任务或点击 “保存” 时被调用。handleDeleteTask(taskId)
在用户点击 “删除” 时被调用。
使用 reducers 管理状态与直接设置状态略有不同。它不是通过设置状态来告诉 React “要做什么”,而是通过事件处理程序 dispatch 一个 “action” 来指明 “用户刚刚做了什么”。(而状态更新逻辑则保存在其他地方!)因此,不再通过事件处理器直接 “设置 task
”,而是 dispatch 一个 “添加/修改/删除任务” 的 action。这更加符合用户的思维。
function handleAddTask(text) {dispatch({type: 'added',id: nextId++,text: text,});
}function handleChangeTask(task) {dispatch({type: 'changed',task: task,});
}
传递给 dispatch
的对象叫做 “action”:
function handleDeleteTask(taskId) {dispatch(// "action" 对象:{type: 'deleted',id: taskId,});
}
它是一个普通的 JavaScript 对象。它的结构是由你决定的,但通常来说,它应该至少包含可以表明 发生了什么事情 的信息。
注意:action 对象可以有多种结构。
按照惯例,通常会添加一个字符串类型的 type
字段来描述发生了什么,并通过其它字段传递额外的信息。type
是特定于组件的,在这个例子中 added
和 addded_task
都可以。选一个能描述清楚发生的事件的名字!
dispatch({// 针对特定的组件type: 'what_happened',// 其它字段放这里
});
第 2 步: 编写一个 reducer 函数
reducer 函数就是放置状态逻辑的地方。它接受两个参数,分别为当前 state 和 action 对象,并且返回的是更新后的 state:
function yourReducer(state, action) {// 给 React 返回更新后的状态
}
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('未知 action: ' + action.type);}
}
由于 reducer
函数接受 state
(tasks)作为参数,因此可以 在组件之外声明它。这减少了代码的缩进级别,提升了代码的可读性。
注意:上面的代码使用了 if/else
语句,但是在 reducers 中使用 switch 语句 是一种惯例。两种方式结果是相同的,但 switch
语句读起来一目了然。
以后会像这样使用:
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);}}
}
建议将每个 case
块包装到 {
和 }
花括号中,这样在不同 case
中声明的变量就不会互相冲突。此外,case
通常应该以 return
结尾。如果忘了 return
,代码就会 进入
到下一个 case
,这就会导致错误!
如果还不熟悉 switch
语句,使用 if/else
也是可以的。
第 3 步: 在组件中使用 reducer
最后,需要将 tasksReducer
导入到组件中。记得先从 React 中导入 useReducer
Hook:
import { useReducer } from 'react';
接下来,就可以替换掉之前的 useState
:
const [tasks, setTasks] = useState(initialTasks);
只需要像下面这样使用 useReducer
:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
useReducer
和 useState
很相似——必须给它传递一个初始状态,它会返回一个有状态的值和一个设置该状态的函数(在这个例子中就是 dispatch 函数)。但是,它们两个之间还是有点差异的。
useReducer
钩子接受 2 个参数:
- 一个 reducer 函数
- 一个初始的 state
它返回如下内容:
- 一个有状态的值
- 一个 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);}}
}...
如果有需要,甚至可以把 reducer 移到一个单独的文件中:
// tasksReducer.js
export default 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);}}
}
当像这样分离关注点时,我们可以更容易地理解组件逻辑。现在,事件处理程序只通过派发 action
来指定 发生了什么,而 reducer
函数通过响应 actions
来决定 状态如何更新。
对比 useState
和 useReducer
Reducers 并非没有缺点!以下是比较它们的几种方法:
- 代码体积: 通常,在使用
useState
时,一开始只需要编写少量代码。而useReducer
必须提前编写 reducer 函数和需要调度的 actions。但是,当多个事件处理程序以相似的方式修改 state 时,useReducer
可以减少代码量。 - 可读性: 当状态更新逻辑足够简单时,
useState
的可读性还行。但是,一旦逻辑变得复杂起来,它们会使组件变得臃肿且难以阅读。在这种情况下,useReducer
允许将状态更新逻辑与事件处理程序分离开来。 - 可调试性: 当使用
useState
出现问题时, 很难发现具体原因以及为什么。 而使用useReducer
时, 可以在 reducer 函数中通过打印日志的方式来观察每个状态的更新,以及为什么要更新(来自哪个action
)。 如果所有action
都没问题,就知道问题出在了 reducer 本身的逻辑中。 然而,与使用useState
相比,必须单步执行更多的代码。 - 可测试性: reducer 是一个不依赖于组件的纯函数。这就意味着可以单独对它进行测试。一般来说,我们最好是在真实环境中测试组件,但对于复杂的状态更新逻辑,针对特定的初始状态和
action
,断言 reducer 返回的特定状态会很有帮助。 - 个人偏好: 并不是所有人都喜欢用 reducer,没关系,这是个人偏好问题。可以随时在
useState
和useReducer
之间切换,它们能做的事情是一样的!
如果在修改某些组件状态时经常出现问题或者想给组件添加更多逻辑时,建议还是使用 reducer。当然,也不必整个项目都用 reducer,这是可以自由搭配的。甚至可以在一个组件中同时使用 useState
和 useReducer
。
编写一个好的 reducers
编写 reducers
时最好牢记以下两点:
- reducers 必须是纯粹的。 这一点和 状态更新函数 是相似的,
reducers
在是在渲染时运行的!(actions 会排队直到下一次渲染)。 这就意味着reducers
必须纯净,即当输入相同时,输出也是相同的。它们不应该包含异步请求、定时器或者任何副作用(对组件外部有影响的操作)。它们应该以不可变值的方式去更新 对象 和 数组。 - 每个 action 都描述了一个单一的用户交互,即使它会引发数据的多个变化。 举个例子,如果用户在一个由
reducer
管理的表单(包含五个表单项)中点击了重置按钮
,那么 dispatch 一个reset_form
的 action 比 dispatch 五个单独的set_field
的 action 更加合理。如果在一个reducer
中打印了所有的action
日志,那么这个日志应该是很清晰的,它能让你以某种步骤复现已发生的交互或响应。这对代码调试很有帮助!
使用 Immer 简化 reducers
与在平常的 state 中 修改对象 和 数组 一样,可以使用 Immer
这个库来简化 reducer
。在这里,useImmerReducer
让你可以通过 push
或 arr[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}/></>);
}...
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 18 迁移状态逻辑至 Reducer 中
参考文章 迁移状态逻辑至 Reducer 中 对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序可能会令人不知所措。对于这种情况,可以将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫作 reducer。 使用 reducer 整合状态逻…...
【SA8295P 源码分析】89 - QNX AIS Camera qcarcam_test 可执行程序 main() 函数 源代码流程分析
【SA8295P 源码分析】89 - QNX AIS Camera qcarcam_test 可执行程序 main 函数 源代码流程分析 一、qcarcam_test.cpp main() 函数源码分析二、qcarcam_test_setup_input_ctxt_thread( ) :初始化 gCtxt.inputs[input_idx] 上下文环境三、process_cb_event_thread( ) :负责处理…...

STM32屏幕计时器
目录 一、最终效果二、实现思想三、实现过程3.1 屏幕显示3.2 中断处理 一、最终效果 显示屏显示计时时间,格式为 00:00:00,依次为 时:分:秒,程序运行之后自动计时,当按下按键,计时清零,按下按键采用外部中…...

MRI多任务技术及应用
目录 一、定量心血管磁共振成像(CMR)的改进方法二、磁共振多任务三、磁共振多任务的成像框架四、磁共振多任务的图像模型和采样和重建策略五、利用MR多任务进行快速三维稳态CEST(ss-CEST)成像5.1 利用MR多任务进行快速三维稳态CEST(ss-CEST)成像介绍5.2 …...

app自动化测试(Android)
Capability 是一组键值对的集合(比如:"platformName": "Android")。Capability 主要用于通知 Appium 服务端建立 Session 需要的信息。客户端使用特定语言生成 Capabilities,最终会以 JSON 对象的形式发送给 …...

【力扣每日一题】2023.9.3 消灭怪物的最大数量
目录 题目: 示例: 分析: 代码: 题目: 示例: 分析: 题目比较长,我概括一下就是有一群怪物,每只怪物离城市的距离都不一样,并且靠近的速度也不一样&#x…...

Python入门教程 | Python3 列表(List)
Python3 列表 序列是 Python 中最基本的数据结构。 序列中的每个值都有对应的位置值,称之为索引,第一个索引是 0,第二个索引是 1,依此类推。 Python 有 6 个序列的内置类型,但最常见的是列表和元组。 列表都可以进…...

Java低代码开发:jvs-list(列表引擎)功能(一)配置说明
在低代码开发平台中,列表页是一个用于显示数据列表的页面。它通常用于展示数据库中的多条记录,并提供搜索、排序和筛选等功能,以方便用户对数据进行查找和浏览。 jvs-list是jvs快速开发平台的列表页的配置引擎,它和普通的crud 具…...

UI自动化之关键字驱动
关键字驱动框架:将每一条测试用例分成四个不同的部分 测试步骤(Test Step):一个测试步骤的描述或者是测试对象的一个操作说明测试步骤中的对象(Test Object):指页面的对象或者元素对象执行的动…...
前端高性能渲染 — 虚拟列表
虚拟列表,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。该技术是解决渲染大量数据的一种解决方法。 实现虚拟列表&…...

防水出色的骨传导耳机,更适合户外运动,南卡Runner Pro 4S体验
已经接近尾声的夏季依然酷热,对于运动爱好者来说,这确实也是锻炼的好时机,无论是一会儿就能大汗淋漓的HIIT,还是是各种清凉的水上运动,在健身的同时,戴上一副耳机享受音乐,都会更加痛快一些。 相…...

docker快速安装-docker一键安装脚本
1.下载/配置安装脚本 touch install-docker.sh #!/bin/bash #mail:ratelcloudqq.com #system:centos7 #integration: docker-latestclear echo "######################################################" echo "# Auto Install Docker …...
1584 - Circular Sequence (UVA)
题目链接如下: Online Judge 我的代码如下: #include <cstdio> #include <string.h> const int maxN 101;int T, len, pivot; char a[maxN];int main(){scanf("%d", &T);for(int i 0; i < T; i){scanf("%s"…...

Revit SDK:Selections 选择
前言 Revit 作为一款成熟的商业软件,它将自己的UI选择功能也通过 API 暴露出来。通过 API 可以按照特定的过滤规则来选择相应的元素,能力和UI基本上是等价的。这个 SDK 用四个例子展示了 API 的能力,内容如下。 内容 PickforDeletion 核心…...
K8s中的RBAC(Role-Based Access Control)
摘要 RBAC(基于角色的访问控制)是一种在Kubernetes中用于控制用户对资源的访问权限的机制。以下是RBAC的设计实现说明: 角色(Role)和角色绑定(RoleBinding):角色定义了一组权限&am…...
肖sir__设计测试用例方法之经验测试方法09_(黑盒测试)
设计测试用例方法之经验测试方法 一、经验的测试技术 (1)基于经验的测试技术之错误推测法 错误推测法也叫错误猜测法,就是根据经验猜想,已有的缺陷,测试经验和失败数据等可能有什么问题并依此设计测试用例 ࿰…...

Python爬虫:下载小红书无水印图片、视频
该代码只提供学习使用,该项目是基于https://github.com/JoeanAmier/XHS_Downloader的小改动 1.下载项目 git clone https://github.com/zhouayi/XHS_Downloader.git2.找到需要下载的文章的ID 写入main.py中 3.下载 python main.py最近很火的莲花楼为例<嘿嘿…...

【小沐学Unity3d】3ds Max 多维子材质编辑(Multi/Sub-object)
文章目录 1、简介2、精简材质编辑器2.1 先创建多维子材质,后指定它2.2 先指定标准材质,后自动创建多维子材质 3、Slate材质编辑器3.1 编辑器简介3.2 编辑器使用 结语 1、简介 多维子材质(Multi/Sub-object)是为一个模形࿰…...
# Go学习-Day8
文章目录 Go学习-Day8单元测试Goroutine进程和线程并发和并行Go协程和主线程MPG模式CPU相关协程并行的资源竞争 Go学习-Day8 个人博客:CSDN博客 单元测试 testing框架会将xxx_test.go的文件引入,调用所有TestXxx的函数 在cal_test.go文件里面写这个 …...

Maven编译java及解决程序包org.apache.logging.log4j不存在问题
1、首先新建一个文件夹,比如hello Hello里新建pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi…...

PPT|230页| 制造集团企业供应链端到端的数字化解决方案:从需求到结算的全链路业务闭环构建
制造业采购供应链管理是企业运营的核心环节,供应链协同管理在供应链上下游企业之间建立紧密的合作关系,通过信息共享、资源整合、业务协同等方式,实现供应链的全面管理和优化,提高供应链的效率和透明度,降低供应链的成…...

理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端
🌟 什么是 MCP? 模型控制协议 (MCP) 是一种创新的协议,旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议,它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...

对WWDC 2025 Keynote 内容的预测
借助我们以往对苹果公司发展路径的深入研究经验,以及大语言模型的分析能力,我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际,我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测,聊作存档。等到明…...

如何在看板中有效管理突发紧急任务
在看板中有效管理突发紧急任务需要:设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP(Work-in-Progress)弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中,设立专门的紧急任务通道尤为重要,这能…...
拉力测试cuda pytorch 把 4070显卡拉满
import torch import timedef stress_test_gpu(matrix_size16384, duration300):"""对GPU进行压力测试,通过持续的矩阵乘法来最大化GPU利用率参数:matrix_size: 矩阵维度大小,增大可提高计算复杂度duration: 测试持续时间(秒&…...

C++ 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...

MySQL 知识小结(一)
一、my.cnf配置详解 我们知道安装MySQL有两种方式来安装咱们的MySQL数据库,分别是二进制安装编译数据库或者使用三方yum来进行安装,第三方yum的安装相对于二进制压缩包的安装更快捷,但是文件存放起来数据比较冗余,用二进制能够更好管理咱们M…...

【 java 虚拟机知识 第一篇 】
目录 1.内存模型 1.1.JVM内存模型的介绍 1.2.堆和栈的区别 1.3.栈的存储细节 1.4.堆的部分 1.5.程序计数器的作用 1.6.方法区的内容 1.7.字符串池 1.8.引用类型 1.9.内存泄漏与内存溢出 1.10.会出现内存溢出的结构 1.内存模型 1.1.JVM内存模型的介绍 内存模型主要分…...
解决:Android studio 编译后报错\app\src\main\cpp\CMakeLists.txt‘ to exist
现象: android studio报错: [CXX1409] D:\GitLab\xxxxx\app.cxx\Debug\3f3w4y1i\arm64-v8a\android_gradle_build.json : expected buildFiles file ‘D:\GitLab\xxxxx\app\src\main\cpp\CMakeLists.txt’ to exist 解决: 不要动CMakeLists.…...
日常一水C
多态 言简意赅:就是一个对象面对同一事件时做出的不同反应 而之前的继承中说过,当子类和父类的函数名相同时,会隐藏父类的同名函数转而调用子类的同名函数,如果要调用父类的同名函数,那么就需要对父类进行引用&#…...