【实战】 九、深入React 状态管理与Redux机制(一) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(十六)
文章目录
- 一、项目起航:项目初始化与配置
- 二、React 与 Hook 应用:实现项目列表
- 三、TS 应用:JS神助攻 - 强类型
- 四、JWT、用户认证与异步请求
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
- 六、用户体验优化 - 加载中和错误状态处理
- 七、Hook,路由,与 URL 状态管理
- 八、用户选择器与项目编辑功能
- 九、深入React 状态管理与Redux机制
- 1.useCallback应用,优化异步请求
- 2.状态提升,组合组件与控制反转
学习内容来源:React + React Hook + TS 最佳实践-慕课网
相对原教程,我在学习开始时(2023.03)采用的是当前最新版本:
项 | 版本 |
---|---|
react & react-dom | ^18.2.0 |
react-router & react-router-dom | ^6.11.2 |
antd | ^4.24.8 |
@commitlint/cli & @commitlint/config-conventional | ^17.4.4 |
eslint-config-prettier | ^8.6.0 |
husky | ^8.0.3 |
lint-staged | ^13.1.2 |
prettier | 2.8.4 |
json-server | 0.17.2 |
craco-less | ^2.0.0 |
@craco/craco | ^7.1.0 |
qs | ^6.11.0 |
dayjs | ^1.11.7 |
react-helmet | ^6.1.0 |
@types/react-helmet | ^6.1.6 |
react-query | ^6.1.0 |
@welldone-software/why-did-you-render | ^7.0.1 |
@emotion/react & @emotion/styled | ^11.10.6 |
具体配置、操作和内容会有差异,“坑”也会有所不同。。。
一、项目起航:项目初始化与配置
- 一、项目起航:项目初始化与配置
二、React 与 Hook 应用:实现项目列表
- 二、React 与 Hook 应用:实现项目列表
三、TS 应用:JS神助攻 - 强类型
- 三、 TS 应用:JS神助攻 - 强类型
四、JWT、用户认证与异步请求
- 四、 JWT、用户认证与异步请求(上)
- 四、 JWT、用户认证与异步请求(下)
五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(上)
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(下)
六、用户体验优化 - 加载中和错误状态处理
- 六、用户体验优化 - 加载中和错误状态处理(上)
- 六、用户体验优化 - 加载中和错误状态处理(中)
- 六、用户体验优化 - 加载中和错误状态处理(下)
七、Hook,路由,与 URL 状态管理
- 七、Hook,路由,与 URL 状态管理(上)
- 七、Hook,路由,与 URL 状态管理(中)
- 七、Hook,路由,与 URL 状态管理(下)
八、用户选择器与项目编辑功能
- 八、用户选择器与项目编辑功能(上)
- 八、用户选择器与项目编辑功能(下)
九、深入React 状态管理与Redux机制
1.useCallback应用,优化异步请求
当前项目中使用 useAsync
进行异步请求,但是其中有一个隐藏 bug
,若是在页面中发起一个请求,这个请求需要较长时间3s(可以使用开发控制台设置请求最短时间来预设场景),在这个时间段内,退出登录,此时就会有报错:
Warning: Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
原因是虽然退出登录,组件销毁,但是异步函数还在执行,当它执行完进行下一步操作 setXXX
或是 更新组件都找不到对应已销毁的组件。
接下来解决一下这个问题。
编辑 src\utils\index.ts
:
...
/*** 返回组件的挂载状态,如果还没有挂载或者已经卸载,返回 false; 反之,返回 true;*/
export const useMountedRef = () => {const mountedRef = useRef(false)useEffect(() => {mountedRef.current = truereturn () => {mountedRef.current = false}}, [])return mountedRef
}
在 src\utils\use-async.ts
上应用:
...
import { useMountedRef } from "utils";
...
export const useAsync = <D>(...) => {...const mountedRef = useMountedRef()...const run = (...) => {...return promise.then((data) => {if(mountedRef.current)setData(data);return data;}).catch((error) => {...});};...
};
还有个遗留问题,在 useEffect
中使用的变量若是没有在依赖数组中添加就会报错,添加上又会造成死循环,因此之前用 eslint-disable-next-line
解决
// eslint-disable-next-line react-hooks/exhaustive-deps
现在换个方案,使用 useMemo
当然可以解决,这里推荐使用特殊版本的 useMemo
, useCallback
:
修改 src\utils\use-async.ts
import { useCallback, useState } from "react";
...export const useAsync = <D>(...) => {...const setData = useCallback((data: D) =>setState({data,stat: "success",error: null,}), [])const setError = useCallback((error: Error) =>setState({error,stat: "error",data: null,}), [])// run 来触发异步请求const run = useCallback((...) => {...}, [config.throwOnError, mountedRef, setData, state, setError],)...
};
可以按照提示配置依赖:
React Hook useCallback has missing dependencies: 'config.throwOnError', 'mountedRef', 'setData', and 'state'. Either include them or remove the dependency array. You can also do a functional update 'setState(s => ...)' if you only need 'state' in the 'setState' call.e
尽管如此,但还是难免会出现,在 useCallback
中改变 依赖值的行为,比如依赖值 XXX
对应的 setXXX
,这时需要用到 setXXX
的函数用法(这样也可以省去一个依赖):
继续修改 src\utils\use-async.ts
...
export const useAsync = <D>(...) => {...const run = useCallback((...) => {...setState(prevState => ({ ...prevState, stat: "loading" }));...}, [config.throwOnError, mountedRef, setData, setError],)...
};
修改 src\utils\project.ts
...
import { useCallback, useEffect } from "react";
...export const useProjects = (...) => {...const fetchProject = useCallback(() =>client("projects", { data: cleanObject(param || {})}), [client, param])useEffect(() => {run(fetchProject(), { rerun: fetchProject });}, [param, fetchProject, run]);...
};
...
修改 src\utils\http.ts
...
import { useCallback } from "react";
...
export const useHttp = () => {...return useCallback((...[funcPath, customConfig]: Parameters<typeof http>) =>http(funcPath, { ...customConfig, token: user?.token }), [user?.token]);
};
总结:非状态类型需要作为依赖 就要将其使用 useMemo
或者 useCallback
包裹(依赖细化 + 新旧关联),常见于 Custom Hook
中函数类型数据的返回
2.状态提升,组合组件与控制反转
接下来定制化一个项目编辑模态框(编辑+新建项目),PageHeader
hover
后可以打开(新建),ProjectList
中可以打开模态框(新建),里面的 List
的每行也可以打开模态框(编辑)
在 src\components\lib.tsx
中新增 padding
为 0
的 Button
:
...
export const ButtonNoPadding = styled(Button)`padding: 0;
`
新建 src\screens\ProjectList\components\ProjectModal.tsx
(模态框):
import { Button, Drawer } from "antd"export const ProjectModal = ({isOpen, onClose}: { isOpen: boolean, onClose: () => void }) => {return <Drawer onClose={onClose} open={isOpen} width="100%"><h1>Project Modal</h1><Button onClick={onClose}>关闭</Button></Drawer>
}
新建 src\screens\ProjectList\components\ProjectPopover.tsx
:
import styled from "@emotion/styled"
import { Divider, List, Popover, Typography } from "antd"
import { ButtonNoPadding } from "components/lib"
import { useProjects } from "utils/project"export const ProjectPopover = ({ setIsOpen }: { setIsOpen: (isOpen: boolean) => void }) => {const { data: projects } = useProjects()const starProjects = projects?.filter(i => i.star)const content = <ContentContainer><Typography.Text type="secondary">收藏项目</Typography.Text><List> {starProjects?.map(project => <List.Item><List.Item.Meta title={project.name}/></List.Item>)}</List><Divider/><ButtonNoPadding type='link' onClick={() => setIsOpen(true)}>创建项目</ButtonNoPadding></ContentContainer>return <Popover placement="bottom" content={content}>项目</Popover>
}const ContentContainer = styled.div`width: 30rem;
`
编辑 src\authenticated-app.tsx
(引入 ButtonNoPadding
、 ProjectPopover
、 ProjectModal
自定义组件,并将模态框的状态管理方法传到对应组件 PageHeader
和 ProjectList
,注意接收方要定义好类型):
...
import { ButtonNoPadding, Row } from "components/lib";
...
import { ProjectModal } from "screens/ProjectList/components/ProjectModal";
import { useState } from "react";
import { ProjectPopover } from "screens/ProjectList/components/ProjectPopover";export const AuthenticatedApp = () => {const [isOpen, setIsOpen] = useState(false)...return (<Container><PageHeader setIsOpen={setIsOpen}/><Main><Router><Routes><Route path="/projects" element={<ProjectList setIsOpen={setIsOpen}/>} />...</Routes></Router></Main><ProjectModal isOpen={isOpen} onClose={() => setIsOpen(false)}/></Container>);
};
const PageHeader = ({ setIsOpen }: { setIsOpen: (isOpen: boolean) => void }) => {...return (<Header between={true}><HeaderLeft gap={true}><ButtonNoPadding type="link" onClick={resetRoute}><SoftwareLogo width="18rem" color="rgb(38,132,255)" /></ButtonNoPadding><ProjectPopover setIsOpen={setIsOpen}/><span>用户</span></HeaderLeft><HeaderRight>...</HeaderRight></Header>);
};
...
由于涉及登录后多个组件会发起调用,因此
ProjectModal
组件需要放在AuthenticatedApp
的Container
下
编辑 src\screens\ProjectList\index.tsx
(引入 模态框的状态管理方法):
...
import { Row, Typography } from "antd";
...
import { ButtonNoPadding } from "components/lib";export const ProjectList = ({ setIsOpen }: { setIsOpen: (isOpen: boolean) => void }) => {...return (<Container><Row justify='space-between'><h1>项目列表</h1><ButtonNoPadding type='link' onClick={() => setIsOpen(true)}>创建项目</ButtonNoPadding></Row>...<ListsetIsOpen={setIsOpen}{...}/></Container>);
};
...
编辑 src\screens\ProjectList\components\List.tsx
(引入 模态框的状态管理方法):
import { Dropdown, MenuProps, Table, TableProps } from "antd";
...
import { ButtonNoPadding } from "components/lib";
...
interface ListProps extends TableProps<Project> {...setIsOpen: (isOpen: boolean) => void;
}export const List = ({ users, setIsOpen, ...props }: ListProps) => {...return (<Tablepagination={false}columns={[...{render: (text, project) => {const items: MenuProps["items"] = [{key: 'edit',label: "编辑",onClick: () => setIsOpen(true)},];return <Dropdown menu={{ items }}><ButtonNoPadding type="link" onClick={(e) => e.preventDefault()}>...</ButtonNoPadding></Dropdown>}}]}{...props}></Table>);
};
可以明显看到,这种方式的状态提升(prop drilling
)若是间隔层数较多时(定义和使用相隔太远),不仅有“下钻”问题,而且耦合度太高
下面使用 组件组合(component composition)的方式解耦
组件组合(component composition) | Context – React
编辑 src\authenticated-app.tsx
(将 绑定了模态框 打开方法的 ButtonNoPadding
作为属性传给需要用到的组件):
...
export const AuthenticatedApp = () => {...return (<Container><PageHeader projectButton={<ButtonNoPadding type="link" onClick={() => setIsOpen(true)}>创建项目</ButtonNoPadding>} /><Main><Router><Routes><Routepath="/projects"element={<ProjectList projectButton={<ButtonNoPadding type="link" onClick={() => setIsOpen(true)}>创建项目</ButtonNoPadding>} />}/>...</Routes></Router></Main>...</Container>);
};
const PageHeader = (props: { projectButton: JSX.Element }) => {...return (<Header between={true}><HeaderLeft gap={true}>...<ProjectPopover { ...props } />...</HeaderLeft><HeaderRight>...</HeaderRight></Header>);
};
...
编辑 src\screens\ProjectList\components\ProjectPopover.tsx
(使用传入的属性组件代替之前的 绑定了模态框 打开方法的 ButtonNoPadding
):
...
export const ProjectPopover = ({ projectButton }: { projectButton: JSX.Element }) => {...const content = (<ContentContainer>...{ projectButton }</ContentContainer>);...
};
...
编辑 src\screens\ProjectList\index.tsx
(使用传入的属性组件代替之前的 绑定了模态框 打开方法的 ButtonNoPadding
并继续“下钻”):
...
export const ProjectList = ({ projectButton }: { projectButton: JSX.Element }) => {...return (<Container><Row justify="space-between">...{ projectButton }</Row>...<ListprojectButton={projectButton}{...}/></Container>);
};
...
编辑 src\screens\ProjectList\components\List.tsx
(使用传入的属性组件代替之前的 绑定了模态框 打开方法的 ButtonNoPadding
):
...
interface ListProps extends TableProps<Project> {...projectButton: JSX.Element
}// type PropsType = Omit<ListProps, 'users'>
export const List = ({ users, ...props }: ListProps) => {...return (<Tablepagination={false}columns={[...{render: (text, project) => {return (<Dropdown dropdownRender={() => props.projectButton}><ButtonNoPaddingtype="link"onClick={(e) => e.preventDefault()}>...</ButtonNoPadding></Dropdown>);},},]}{...props}></Table>);
};
- 编辑按钮这里使用并不恰当,不过这不是最终解决方案,理解思路即可
- 浅析控制反转 - 知乎
部分引用笔记还在草稿阶段,敬请期待。。。
相关文章:
【实战】 九、深入React 状态管理与Redux机制(一) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(十六)
文章目录 一、项目起航:项目初始化与配置二、React 与 Hook 应用:实现项目列表三、TS 应用:JS神助攻 - 强类型四、JWT、用户认证与异步请求五、CSS 其实很简单 - 用 CSS-in-JS 添加样式六、用户体验优化 - 加载中和错误状态处理七、Hook&…...
第九十五回 如何使用dio的转换器
文章目录 概念介绍使用方法使用默认的转换器自定义转换器 示例代码经验分享 我们在上一章回中介绍了"如何打造一个网络框架"相关的内容,本章回中将介绍 如何使用dio的转换器.闲话休提,让我们一起Talk Flutter吧。 概念介绍 转换器主要用来转…...

Python深度学习“四大名著”之一【赠书活动|第二期《Python机器学习:基于PyTorch和Scikit-Learn》】
近年来,机器学习方法凭借其理解海量数据和自主决策的能力,已在医疗保健、 机器人、生物学、物理学、大众消费和互联网服务等行业得到了广泛的应用。自从AlexNet模型在2012年ImageNet大赛被提出以来,机器学习和深度学习迅猛发展,取…...

RAID相关知识
简介 RAID ( Redundant Array of Independent Disks )即独立磁盘冗余阵列,通常简称为磁盘阵列。RAID技术将多个单独的物理硬盘以不同的方式组合成一个逻辑磁盘,从而提高硬盘的读写性能和数据安全性。 数据组织形式 分块&#x…...

DataStructure--Basic
程序设计数据结构算法 只谈数据结构不谈算法就跟去话剧院看梁山伯与祝英台结果只有梁山伯在演,祝英台生病了没来一样。 本文的所有内容都出自《大话数据结构》这本书中的代码实现部分,建议看书,书中比我本文写的全。 数据结构,直…...

Intellij IDEA 双击启动报错ClassNotFoundException: com.licel.b.z@
项目场景: 新从官网下载了ideaIU-2023.2.win.zip ,安装后双击启动报错, 无法运行idea, 提示信息如下 问题描述 Internal error. Please refer to https://jb.gg/ide/critical-startup-errorsjava.lang.ExceptionInInitializerErrorat java…...

使用 Logstash 及 enrich processor 实现数据丰富自动化
在我之前的文章: Elasticsearch:enrich processor (7.5发行版新功能) Elasticsearch:使用 Elasticsearch ingest pipeline 丰富数据 通过上面的两篇文章的介绍,我们应该充分掌握了如何使用 enrich proce…...

Django模板语法和请求
1、在django关于模板文件加载顺序 创建的django项目下会有一个seeetings.py的文件 如果在seeetings.py 中加了 os.path.join(BASE_DIR,‘templates’),如果是pycharm创建的django项目会加上,就会默认先去根目录找templates目录下的html文件,…...

Android跨进程传大图思考及实现——附上原理分析
1.抛一个问题 这一天,法海想锻炼小青的定力,由于Bitmap也是一个Parcelable类型的数据,法海想通过Intent给小青传个特别大的图片 intent.putExtra("myBitmap",fhBitmap)如果“法海”(Activity)使用Intent去传递一个大的Bitmap给“…...

【动态规划part13】| 300.最长递增子序列、674.最长连续递增序列、718.最长重复数组
目录 🎈LeetCode 300.最长递增子序列 🎈LeetCode 674. 最长连续递增序列 🎈LeetCode 718. 最长重复子数组 🎈LeetCode 300.最长递增子序列 链接:300.最长递增子序列 给你一个整数数组 nums ,找到其…...

QMainWindow
文章目录 QMainWindow基本元素QMainWindow函数介绍简单的示例效果图 QMainWindow QMainWindow是一个为用户提供主窗口程序 的类,包含一个菜单栏(menu bar)、多个工具栏 (tool bars)、多个锚接部件(dock widgets)、―个 状态栏(status bar )及一个中心部件(central …...

PV操作解决经典进程同步问题
一.经典同步问题 在学习《操作系统》时,会接触到进程的概念,其中不可避免的接触到进程同步问题,今天我们用熟悉的PV操作解决一些经典的进程同步问题。 二.生产者-消费者问题 1.问题描述 问题描述:一组生产者进程和一组消费者进…...

一文3000字从0到1使用Selenium进行自动化测试
对于很多刚入门的测试新手来说,大家都将自动化测试作为自己职业发展的一个主要阶段。可是,在成为一名合格的自动化测试工程师之前,我们不仅要掌握相应的理论知识,还要进行大量的实践,积累足够的经验,以便快…...

基于开源IM即时通讯框架MobileIMSDK:RainbowChat v9.0版已发布
关于MobileIMSDK MobileIMSDK 是一套专门为移动端开发的开源IM即时通讯框架,超轻量级、高度提炼,一套API优雅支持UDP 、TCP 、WebSocket 三种协议,支持iOS、Android、H5、标准Java平台,服务端基于Netty编写。 工程开源地址是&am…...

交叉编译----宿主机x86 ubuntu 64位-目标机ARMv8 aarch64
1.交叉编译是什么,为什么要交叉编译 编译:在一个平台上生成在该平台上的可执行代码交叉编译:在一个平台上生成在另一个平台上的可执行代码交叉编译的例子:如51单片机的可执行代码(hex文件)是在集成环境kei…...

安防监控视频汇聚平台EasyCVR修改录像计划等待时间较长是什么原因?
安防监控视频EasyCVR视频融合汇聚平台基于云边端智能协同,支持海量视频的轻量化接入与汇聚、转码与处理、全网智能分发等。音视频流媒体视频平台EasyCVR拓展性强,视频能力丰富,具体可实现视频监控直播、视频轮播、视频录像、云存储、回放与检…...
深度学习调参指南
1. 选择合适的模型架构 模型的结构(层数和宽度),参数配置,尽量用已经有效的模型 2. 选择优化器 针对具体的问题,从选择常用的优化器开始,进行比较 3. 选择BatchSize 1). Batch Size决定训练速度,但是不影响验证集…...
MYSQL 优化常用方法
1、选取最适用的字段属性 MySQL可以很好的支持大数据量的存取,但是一般说来,数据库中的表越小,在它上面执行的查询也就会越快。因此,在创建表的时候,为了获得更好的性能,我们可以将表中字段的宽度设得尽可…...

isp调试工具环境搭建及其介绍!
一、isp调试环境搭建: 后期调试isp,是在rv1126提供的RKISP2.x Tuner工具上进行调试,所以我们大前提必须要把这个环境和一些操作先搞熟悉来,后面有一些专用术语,我们遇到了再去看,现在专门看一些专用术语&am…...

word显示书签并给书签添加颜色
CTRg 定位书签 在 Word 的用户界面中,没有直接的选项可以批量为所有书签设置颜色。但你可以使用 VBA 宏或者编写自定义的功能来实现这个需求。这里给出一个简单的 VBA 宏,它可以设置当前文档中所有书签内文本的颜色:vba Sub ColorAllBookmark…...
Vue记事本应用实现教程
文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展:显示创建时间8. 功能扩展:记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...
SkyWalking 10.2.0 SWCK 配置过程
SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外,K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案,全安装在K8S群集中。 具体可参…...

stm32G473的flash模式是单bank还是双bank?
今天突然有人stm32G473的flash模式是单bank还是双bank?由于时间太久,我真忘记了。搜搜发现,还真有人和我一样。见下面的链接:https://shequ.stmicroelectronics.cn/forum.php?modviewthread&tid644563 根据STM32G4系列参考手…...

Day131 | 灵神 | 回溯算法 | 子集型 子集
Day131 | 灵神 | 回溯算法 | 子集型 子集 78.子集 78. 子集 - 力扣(LeetCode) 思路: 笔者写过很多次这道题了,不想写题解了,大家看灵神讲解吧 回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili 完…...

CentOS下的分布式内存计算Spark环境部署
一、Spark 核心架构与应用场景 1.1 分布式计算引擎的核心优势 Spark 是基于内存的分布式计算框架,相比 MapReduce 具有以下核心优势: 内存计算:数据可常驻内存,迭代计算性能提升 10-100 倍(文档段落:3-79…...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...
拉力测试cuda pytorch 把 4070显卡拉满
import torch import timedef stress_test_gpu(matrix_size16384, duration300):"""对GPU进行压力测试,通过持续的矩阵乘法来最大化GPU利用率参数:matrix_size: 矩阵维度大小,增大可提高计算复杂度duration: 测试持续时间(秒&…...
Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信
文章目录 Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信前言一、网络通信基础概念二、服务端与客户端的完整流程图解三、每一步的详细讲解和代码示例1. 创建Socket(服务端和客户端都要)2. 绑定本地地址和端口&#x…...
JavaScript 数据类型详解
JavaScript 数据类型详解 JavaScript 数据类型分为 原始类型(Primitive) 和 对象类型(Object) 两大类,共 8 种(ES11): 一、原始类型(7种) 1. undefined 定…...
【JavaSE】多线程基础学习笔记
多线程基础 -线程相关概念 程序(Program) 是为完成特定任务、用某种语言编写的一组指令的集合简单的说:就是我们写的代码 进程 进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存…...