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项目到服务器后404错误的原因及解决方案

《部署Vue项目到服务器后404错误的原因及解决方案》文章介绍了Vue项目部署步骤以及404错误的解决方案,部署步骤包括构建项目、上传文件、配置Web服务器、重启Nginx和访问域名,404错误通常是... 目录一、vue项目部署步骤二、404错误原因及解决方案错误场景原因分析解决方案一、Vue项目部署步骤

前端原生js实现拖拽排课效果实例

《前端原生js实现拖拽排课效果实例》:本文主要介绍如何实现一个简单的课程表拖拽功能,通过HTML、CSS和JavaScript的配合,我们实现了课程项的拖拽、放置和显示功能,文中通过实例代码介绍的... 目录1. 效果展示2. 效果分析2.1 关键点2.2 实现方法3. 代码实现3.1 html部分3.2

CSS弹性布局常用设置方式

《CSS弹性布局常用设置方式》文章总结了CSS布局与样式的常用属性和技巧,包括视口单位、弹性盒子布局、浮动元素、背景和边框样式、文本和阴影效果、溢出隐藏、定位以及背景渐变等,通过这些技巧,可以实现复杂... 一、单位元素vm 1vm 为视口的1%vh 视口高的1%vmin 参照长边vmax 参照长边re

CSS3中使用flex和grid实现等高元素布局的示例代码

《CSS3中使用flex和grid实现等高元素布局的示例代码》:本文主要介绍了使用CSS3中的Flexbox和Grid布局实现等高元素布局的方法,通过简单的两列实现、每行放置3列以及全部代码的展示,展示了这两种布局方式的实现细节和效果,详细内容请阅读本文,希望能对你有所帮助... 过往的实现方法是使用浮动加

css渐变色背景|<gradient示例详解

《css渐变色背景|<gradient示例详解》CSS渐变是一种从一种颜色平滑过渡到另一种颜色的效果,可以作为元素的背景,它包括线性渐变、径向渐变和锥形渐变,本文介绍css渐变色背景|<gradien... 使用渐变色作为背景可以直接将渐China编程变色用作元素的背景,可以看做是一种特殊的背景图片。(是作为背

CSS自定义浏览器滚动条样式完整代码

《CSS自定义浏览器滚动条样式完整代码》:本文主要介绍了如何使用CSS自定义浏览器滚动条的样式,包括隐藏滚动条的角落、设置滚动条的基本样式、轨道样式和滑块样式,并提供了完整的CSS代码示例,通过这些技巧,你可以为你的网站添加个性化的滚动条样式,从而提升用户体验,详细内容请阅读本文,希望能对你有所帮助...

css实现图片旋转功能

《css实现图片旋转功能》:本文主要介绍了四种CSS变换效果:图片旋转90度、水平翻转、垂直翻转,并附带了相应的代码示例,详细内容请阅读本文,希望能对你有所帮助... 一 css实现图片旋转90度.icon{ -moz-transform:rotate(-90deg); -webkit-transfo

vue基于ElementUI动态设置表格高度的3种方法

《vue基于ElementUI动态设置表格高度的3种方法》ElementUI+vue动态设置表格高度的几种方法,抛砖引玉,还有其它方法动态设置表格高度,大家可以开动脑筋... 方法一、css + js的形式这个方法需要在表格外层设置一个div,原理是将表格的高度设置成外层div的高度,所以外层的div需要

Vue项目中Element UI组件未注册的问题原因及解决方法

《Vue项目中ElementUI组件未注册的问题原因及解决方法》在Vue项目中使用ElementUI组件库时,开发者可能会遇到一些常见问题,例如组件未正确注册导致的警告或错误,本文将详细探讨这些问题... 目录引言一、问题背景1.1 错误信息分析1.2 问题原因二、解决方法2.1 全局引入 Element

详解如何在React中执行条件渲染

《详解如何在React中执行条件渲染》在现代Web开发中,React作为一种流行的JavaScript库,为开发者提供了一种高效构建用户界面的方式,条件渲染是React中的一个关键概念,本文将深入探讨... 目录引言什么是条件渲染?基础示例使用逻辑与运算符(&&)使用条件语句列表中的条件渲染总结引言在现代