【实战】 九、深入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
prettier2.8.4
json-server0.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 中新增 padding0Button

...
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(引入 ButtonNoPaddingProjectPopoverProjectModal 自定义组件,并将模态框的状态管理方法传到对应组件 PageHeaderProjectList,注意接收方要定义好类型):

...
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 组件需要放在 AuthenticatedAppContainer

编辑 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>);
};
  • 编辑按钮这里使用并不恰当,不过这不是最终解决方案,理解思路即可
  • 浅析控制反转 - 知乎

部分引用笔记还在草稿阶段,敬请期待。。。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/14931.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

jenkinsfile小试牛刀

序 本文主要演示一下如何用jenkinsfile来编译java服务 安装jenkins 这里使用docker来安装jenkins docker run --name jenkins-docker \ --volume $HOME/jenkins_home:/var/jenkins_home \ -p 8080:8080 jenkins/jenkins:2.416之后访问http://${yourip}:8080&#xff0c;然后…

华为OD机试真题 Java 实现【报文回路】【2023 B卷 100分】,俗称“礼尚往来”

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路1、报文回路2、异常情况&#xff1a;3、解题思路 五、Java算法源码六、效果展示1、输入2、输出 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&…

FPGA学习——FPGA利用状态机实现电子锁模拟

文章目录 一、本次实验简介二、源码及分析三、总结 一、本次实验简介 本次是实验是为了利用状态机模拟电子锁&#xff0c;相关要求如下&#xff1a; 顺序输入4位密码&#xff0c;密码为1234&#xff0c;用按键来键入密码用led灯指示键入第几位密码&#xff0c;&#xff08;博…

C++模拟实现反向迭代器

1.代码实现 1.有了解正向迭代器的应该知道&#xff0c;比如list的正向迭代器其实本质是一个类&#xff0c;而有些人想模拟实现反向迭代器&#xff0c;依旧想再创建一个类&#xff0c;但是库里面想要的是&#xff0c;你给我一个迭代器&#xff0c;我就能给你反馈一个反向迭代器…

word显示书签并给书签添加颜色

CTRg 定位书签 在 Word 的用户界面中&#xff0c;没有直接的选项可以批量为所有书签设置颜色。但你可以使用 VBA 宏或者编写自定义的功能来实现这个需求。这里给出一个简单的 VBA 宏&#xff0c;它可以设置当前文档中所有书签内文本的颜色&#xff1a;vba Sub ColorAllBookmark…

Gromacs-plumed元动力学

标准的meta动力学模拟需要设定如下标准参数 metad: ARG__FILL__ PACE__FILL__ HEIGHT__FILL__ BIASFACTOR__FILL__ SIGMA__FILL__ FILE__FILL__ GRID_MIN__FILL__ GRID_MAX__FILL__ GRID_BIN__FILL__ TEMP__FILL__ARG: 需要添加高斯偏置势的集合变量CV (e.g. distance, angle,…

MySQL基于复制线程实现MTS并行恢复binlog

文章目录 一、MySQL备份恢复流程二、并行恢复binlog原理三、操作步骤四、总结 一、MySQL备份恢复流程 MySQL数据恢复通常分为两个步骤&#xff1a; 恢复全备数据&#xff0c;MySQL有多种备份工具&#xff0c;分为物理备份和逻辑备份&#xff1b;具体可以参看下面这篇文章 MySQL…

零代码编程:PDF文件名和Excel数据进行比对找不同

F盘“北交所招股说明书”文件夹下有150个文件&#xff1b; F盘”北证A股20230703.xlsx”表格中证券名称有200多个&#xff1b; 现在想找出文件夹下的哪些证券名称不在表格里面。 在ChatGPT中输入提示词&#xff1a; 写一段Python程序&#xff1a; F盘“北交所招股说明书”文…

ElementUI Select选择器如何根据value值显示对应的label

修改前效果如图所示&#xff0c;数据值状态应显示为可用&#xff0c;但实际上仅显示了状态码1&#xff0c;并没有显示其对应的状态信息。在排查了数据类型对应关系问题后&#xff0c;并没有产生实质性影响&#xff0c;只好对代码进行了如下修改。 修改前代码&#xff1a; <…

Python自动化测试----生成测试报告

如何才能让用例自动运行完之后&#xff0c;生成一张直观可看易懂的测试报告呢&#xff1f; 对于自动化测试有兴趣的朋友可以观看这个视频&#xff1a; 【整整200集】超超超详细的Python接口自动化测试进阶教程&#xff0c;真实模拟企业项目实战&#xff01;&#xff01; 小编使…

【暑期每日一练】 day9

目录 选择题 &#xff08;1&#xff09; 解析&#xff1a; &#xff08;2&#xff09; 解析&#xff1a; &#xff08;3&#xff09; 解析&#xff1a; &#xff08;4&#xff09; 解析&#xff1a; &#xff08;5&#xff09; 解析&#xff1a; 编程题 题一 …

python学习之【浅拷贝】

前言 上一篇文章&#xff0c;python学习之【继承、封装、多态】主要学习了面向对象的三大特征。这篇文章记录下对python的浅拷贝的学习&#xff0c;下一篇文章接着学习深拷贝。 简单了解 浅拷贝&#xff1a;python拷贝一般都是浅拷贝&#xff0c;拷贝时&#xff0c;对象包含的…

ARM基础(6):内存屏障指令之DMB、DSB和ISB详解

内存屏障是一个通用术语&#xff0c;用于指代一条或多条指令&#xff0c;它们强制处理器在执行加载(load)或存储(store)指令时进行同步事件。ARMv7-M 和 ARMv6-M架构都提供了三个内存屏障指令来支持内存顺序模型。这三个内存屏障指令分别是&#xff1a;DMB、DSB和ISB。 文章目录…

零信任安全解决方案

什么是零信任 零信任网络架构 &#xff08;ZTNA&#xff09; 或零信任安全是一种新的组织网络安全方法。它旨在修复传统基于边界的安全性中的缺陷并简化网络设计。 它以“永不信任&#xff0c;始终验证”的原则运作。这意味着&#xff0c;无论用户或设备位于何处&#xff0c;…

位运算 剑指offer15 二进制中1的个数 搜索算法:55-II 平衡二叉树 数值的整数次方 39数组中出现次数超过一半的数字

可能会引起死循环的解法&#xff1a; 看最右边一位是不是1&#xff0c;然后将输入的整数右移一位&#xff0c;再判断最右边一位&#xff08;即倒数第二位&#xff09;是否为1&#xff0c;接着再右移&#xff0c;知道整数移动到0为止 这个解法&#xff0c;把整数右移一位和把整数…

TCP网络通信编程之字符流

【案例1】 【题目描述】 【 注意事项】 (3条消息) 节点流和处理流 字符处理流BufferedReader、BufferedWriter&#xff0c;字节处理流-BufferedInputStream和BufferedOutputStream (代码均正确且可运行_Studying~的博客-CSDN博客 1。这里需要使用字符处理流&#xff0c;来将…

wxwidgets Ribbon构建多个page与按钮响应

新建一个控制台应用程序&#xff0c;添加好头文件的依赖与lib库文件的依赖&#xff0c;修改属性&#xff1a; 将进入ribbon界面的文件与主界面的类分开&#xff1a; 1、RibbonSample.cpp #include "stdafx.h" #include "MyFrame.h" class MyApp : public…

集成测试,单元测试隔离 maven-surefire-plugin

详见 集成测试,单元测试隔离 maven-surefire-plugin maven的goal生命周期 Maven生存周期 - 含 integration-test Maven本身支持的命令&#xff08;Goals&#xff09;是有顺序的&#xff0c;越后面执行的命令&#xff0c;会将其前面的命令和其本身按顺序执行一遍&#xff0c;…

AD从原理图到PCB超详细教程

AD超详细教程 前言一、建立一个工程模板二、原理图1.设计原理图。2.使用AD自带库和网上开源原理图库3.画原理图库4.编译原理图 三、PCB1.确定元器件尺寸大小2.绘制PCB Library①使用元器件向导绘制元件库②原理图与PCB的映射 3.绘制PCB①更新PCB②调整元件位置③布线④漏线检查…

c++函数式编程:统计文件字符串,文件流

头文件 #include <iostream> #include <fstream> #include <string> #include <sstream> #include <algorithm> #include <vector>统计方法 int count_lines(const std::string &filename) {std::ifstream in{filename};return std:…