【实战】 八、用户选择器与项目编辑功能(上) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(十四)

文章目录

    • 一、项目起航:项目初始化与配置
    • 二、React 与 Hook 应用:实现项目列表
    • 三、TS 应用:JS神助攻 - 强类型
    • 四、JWT、用户认证与异步请求
    • 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
    • 六、用户体验优化 - 加载中和错误状态处理
    • 七、Hook,路由,与 URL 状态管理
    • 八、用户选择器与项目编辑功能
      • 1.实现id-select.tsx解决id类型 难题
      • 2.抽象user-select组件选择用户
      • 3.自定义 Star 组件做项目收藏标记


学习内容来源: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 状态管理(下)

八、用户选择器与项目编辑功能

1.实现id-select.tsx解决id类型 难题

上一节最后的 bug 可以通过自定义组件来优化掉

新建 src\types\index.ts 存放常用类型:

export type SN = string | number

新建组件 src\components\id-select.tsx

import { Select } from "antd"
import { SN } from "types"type SelectProps = React.ComponentProps<typeof Select>// 类型不是简单的后来者居上,而是寻求"最大公约数"的方式
interface IdSelectProps extends Omit<SelectProps, 'value' | 'onChange' | 'options'>{value: SN | null | undefined,onChange: (value?: number) => void,defaultOptionName?: string,options?: {name: string, id: number}[]
}/*** value 可以传入多种类型的值* onChange 只会回调 number | undefined 类型* 当isNaN(Number(value)) 为 true 的时候,代表选择默认类型* 当选择默认类型时,onChange 会回调 undefined* @param props */
export const IdSelect = (props: IdSelectProps) => {const { value, onChange, defaultOptionName, options, ...restProps } = propsreturn <Selectvalue={toNumber(value)}onChange={value => onChange(toNumber(value) || undefined)}{ ...restProps }>{defaultOptionName ? <Select.Option value={0}>{defaultOptionName}</Select.Option> : null}{options?.map(option => <Select.Option key={option.id} value={option.id}>{option.name}</Select.Option>)}</Select>
}const toNumber = (value: unknown) => isNaN(Number(value)) ? 0 : Number(value)

2.抽象user-select组件选择用户

修改 src\screens\ProjectList\components\List.tsx(将 Project 中的 idpersonId 类型统一改为 number):

...
export interface Project {id: number;...personId: number;...
}
...

修改 src\screens\ProjectList\components\SearchPanel.tsx(将 User 中的 id 改为 number 类型,使用Utility Types处理 Project 类型 生成 param 的可选子类型):

...
import { Project } from "./List";export interface User {id: number;...
}
interface SearchPanelProps {...param: Partial<Pick<Project, 'name' | 'personId'>>...
}
...
  • Partial:将每个子类型转换为可选类型
  • Pick:经过 泛型约束 生成一个新类型

由于从 URL 中得到的数据都是 string 类型,因此需要特殊处理,接下来将这部分单独抽离出来

新建 src\screens\ProjectList\utils.ts

import { useUrlQueryParam } from "utils/url";export const useProjectsSearchParams = () => {const [param, setParam] = useUrlQueryParam(["name", "personId"]);return [{...param, personId: Number(param.personId) || undefined},setParam] as const
}

src\screens\ProjectList\index.tsx 中调用它:

...
import { useProjectsSearchParams } from "./utils";export const ProjectList = () => {useDocumentTitle('项目列表')const [param, setParam] = useProjectsSearchParams()...
};
...

接下来重头戏来了

新建 src\components\user-select.tsx

import { useUsers } from "utils/use-users";
import { IdSelect } from "./id-select";export const UserSelect = (props: React.ComponentProps<typeof IdSelect>) => {const {data: users} = useUsers()return <IdSelect options={users || []} {...props}/>
};

src\screens\ProjectList\components\SearchPanel.tsx 中调用 UserSelect 组件:

...
import { UserSelect } from "components/user-select";...
export const SearchPanel = ({ users, param, setParam }: SearchPanelProps) => {return (<Form {...}>...<Form.Item><UserSelectdefaultOptionName="负责人"value={param.personId}onChange={(value) => setParam({ ...param, personId: value, })}/></Form.Item></Form>);
};

查看页面效果,又发生了熟悉的事情。。。无限循环

打开 wdyr 的开关,查找原因,发现之前的 useUrlQueryParam 中的 param 使用 useMemo 后不再创建新对象,但是经过 useProjectsSearchParams 处理,每次返回的又是新对象,那还是老办法,用 useMemo 解决

修改 src\screens\ProjectList\utils.ts

import { useMemo } from "react";
...
// 项目列表搜索的参数
export const useProjectsSearchParams = () => {...return [useMemo(() =>({...param, personId: Number(param.personId) || undefined}), [param]),setParam] as const
}

查看页面,问题解决

还有个特别小的问题,一般情况下容易忽略:

  • 当切换到某个具体负责人时,刷新页面(带参链接首次加载)时,userSelect 组件在 users 数据请求回来之前由于找不到匹配项,会短暂显示 personId

接下来解决一下

修改 src\components\id-select.tsx(请求到 users 数据之前值为 0,即显示默认选项负责人):

...
export const IdSelect = (props: IdSelectProps) => {...return (<Selectvalue={options?.length ? toNumber(value) : 0}{...}>...</Select>);
};
...

查看页面效果,完美!

3.自定义 Star 组件做项目收藏标记

为每个项目新增一个收藏标记

新建组件 Star src\components\star.tsx

import { Rate } from "antd";interface StarProps extends React.ComponentProps<typeof Rate> {checked: boolean,onCheckedChange?: (checked: boolean) => void
}export const Star = ({checked, onCheckedChange, ...restProps}: StarProps) => {return <Ratecount={1}value={checked ? 1 : 0}onChange={num => onCheckedChange?.(!!num)}{...restProps}/>
}
  • 注意组件原有属性的透传
  • 评分 Rate - Ant Design

新增 编辑和新增 的 Custom Hook src\utils\project.ts

...
export const useEditProject = () => {const client = useHttp();const { run, ...asyncResult } = useAsync<Project[]>();const mutate = (params: Partial<Project>) => {return run(client(`projects/${params.id}`, {data: params,method: 'PATCH'}))}return {mutate,...asyncResult};
};export const useAddProject = () => {const client = useHttp();const { run, ...asyncResult } = useAsync<Project[]>();const mutate = (params: Partial<Project>) => {return run(client(`projects/${params.id}`, {data: params,method: 'POST'}))}return {mutate,...asyncResult};
};

这部分在构思时需要考虑到,Hook 只能在 函数组件内的最外层使用,不能在外面再嵌套其他非组件的普通函数,因此需要提前暴露出一个函数来接收参数并处理相关逻辑(闭包的应用),否则会出现下面的报错:

  • React Hook "useEditProject" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.

编辑 src\screens\ProjectList\components\List.tsx(使用 Star 组件):

...
import { Star } from "components/star";
import { useEditProject } from "utils/project";
...
// type PropsType = Omit<ListProps, 'users'>
export const List = ({ users, ...props }: ListProps) => {const { mutate } = useEditProject()// 函数式编程 柯里化const starProject = (id: number) => (star: boolean) => mutate({id, star})return (<Tablepagination={false}columns={[{title: <Star checked={true} disabled={true}/>,render: (val, record) =><Starchecked={record.star}// stared => starProject(record.id)(stared)onCheckedChange={starProject(record.id)}/>},...]}{...props}></Table>);
};
  • 在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
  • 柯里化(currying)又称部分求值。一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

查看页面,点击标记一个,但是没有反应,控制台 Network 中有网络请求,刷新页面再看,数据已经更新了,这个问题后续解决


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

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

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

相关文章

Go语言基础语法

Go语言基础语法 Hello World变量&常量定义if 判断语句for 循环语句&#xff08;只有for&#xff09;switch 选择语句数组&#xff08;较少使用&#xff09;切片&#xff08;较多使用&#xff09;map&#xff08;实际最常用&#xff09;range函数指针结构体结构体方法错误处…

23 自定义控件

案例&#xff1a;组合Spin Box和Horizontal Slider实现联动 新建Qt设计师界面&#xff1a; 选择Widget&#xff1a; 选择类名&#xff08;生成.h、.cpp、.ui文件&#xff09; 在smallWidget.ui中使用Spin Box和Horizontal Slider控件 可以自定义数字区间&#xff1a; 在主窗口w…

Docker 单机/集群 部署 Nacos2.2.0

单机部署 1- 拉取镜像 docker pull nacos/nacos-server:v2.2.02- 准备挂载的配置文件目录和日志目录 日志目录(空目录)&#xff1a;./nacos/logs配置文件&#xff1a;./nacos/conf/application.properties 从官网下载 nacos 压缩包&#xff1a;Release 2.2.0 (Dec 14, 2022…

sql进阶:求满足某列数值相加无限接近90%的行(90分位)

sql 一、案例分析二、思路三、代码实现一、案例分析 表中有某个id列和数值列,求数值列占比为90%的id,如有个用户表,存储id和消费金额order_cnt,求一条sql查出消费占比无限接近90%的所有客户,如表中总消费为10000,占比最高的是4000、3000、2800,对应A、B、C用户,查出A、B、C用户…

18.背景轮播

背景轮播 html部分 <div class"container"><div class"slide active" style"background-image: url(./static/20180529205331_yhGyf.jpeg);"></div><div class"slide " style"background-image: url(./s…

【数据挖掘】使用 LSTM 进行时间和序列预测

一、说明 每天&#xff0c;人类在执行诸如过马路之类的任务时都会做出被动预测&#xff0c;他们估计汽车的速度和与汽车的距离&#xff0c;或者通过猜测球的速度并相应地定位手来接球。这些技能是通过经验和实践获得的。然而&#xff0c;由于涉及众多变量&#xff0c;预测天气或…

P3382 【模板】三分法

三分其实是每次取L,R的终点mid&#xff0c;把mid左边一点点的函数值和右边一点点的函数值比较&#xff0c;舍弃一边的区间&#xff0c;这样不断缩小区间直到满足精度要求(一般eps取0.1*精度)&#xff0c;但我们都喜欢取三等分点&#xff0c;其实只要是左边一点点和右边一点点就…

linux 升级node

linux环境在执行npm ci是报错&#xff0c;提示node版本太低&#xff0c;故需升级&#xff0c;升级过程如下&#xff1a; 1. npm cache clean -f 2. npm install -g n 3. n stable # 下载最新的稳定的版本 上面第3行结果如下&#xff1a; installing : node-v20.5.0 mkdi…

SpringCloudAlibaba微服务实战系列(二)Nacos配置中心

SpringCloudAlibaba Nacos配置中心 在java代码中或者在配置文件中写配置&#xff0c;是最不雅的&#xff0c;意味着每次修改配置都需要重新打包或者替换class文件。若放在远程的配置文件中&#xff0c;每次修改了配置后只需要重启一次服务即可。话不多说&#xff0c;直接干货拉…

NoSQL之 Redis配置与优化

文章目录 一.关系数据库与非关系型数据库1.关系型数据库2.非关系型数据库3.关系型数据库和非关系型数据库区别4.非关系型数据库产生背景 二.Redis简介1.了解Redis2.Redis 具有以下几个优点3.Redis为何这么快 三.Redis 安装及应用1.Redis 安装部署2.Redis 命令工具2.1 redis-cli…

数字化新移民的转型之路探析

数字化转型&#xff0c;本质还是转型 很多企业谈到数字化转型时往往会对技术产生敬畏心理&#xff0c;与之伴随的后续动作往往是过度关注科技。但不重视与任用员工&#xff0c;再好的AI、算法、大数据等也发挥不了威力。 人和组织方面的问题&#xff0c;是导致很多企业的数字…

会点C++还需要再学Python吗?

提到的C、数据结构与算法、操作系统、计算机网络和数据库技术等确实是计算机科学中非常重要的基础知识领域&#xff0c;对于软件开发和计算机工程师来说&#xff0c;它们是必备的核心知识。掌握这些知识对于开发高性能、可靠和安全的应用程序非常重要。Python作为一种脚本语言&…

运动蓝牙耳机什么牌子的好用、最好用的运动蓝牙耳机推荐

音乐是运动的灵魂&#xff0c;而一款优秀的运动耳机则是让音乐与我们的身体完美融合的关键。今天&#xff0c;我推荐五款备受运动爱好者喜爱的耳机&#xff0c;它们以卓越的音质、舒适的佩戴和出色的稳定性能脱颖而出&#xff0c;助你在运动中创造最佳状态。 1、NANK南卡Runne…

Langchain 的 SimpleSequentialChain 和 SequentialChain

Langchain 的 SimpleSequentialChain 和 SequentialChain 1. SimpleSequentialChain2. SequentialChain3. Memory in Sequential Chains 1. SimpleSequentialChain 调用语言模型后的下一步是对语言模型进行一系列调用。当您想要获取一个调用的输出并将其用作另一个调用的输入时…

安全第一天

1. 编码 1.1 ASCLL编码 ASCII 是基于拉丁字母的一套电脑编码系统&#xff0c;主要用于显示现代英语和其他西欧语言。它是最通用的信息交换标准&#xff0c;并等同于国际标准ISO/IEC 646。 1.2 URL编码 URL&#xff1a;&#xff08;统一资源定位器、定位地址&#xff0c;俗称网页…

k8s常见的资源对象使用

目录 一、kubernetes内置资源对象 1.1、kubernetes内置资源对象介绍 1.2、kubernetes资源对象操作命令 二、job与cronjob计划任务 2.1、job计划任务 2.2、cronjob计划任务 三、RC/RS副本控制器 3.1、RC副本控制器 3.2、RS副本控制器 3.3、RS更新pod 四、Deployment副…

分布式协议Raft和Paxos详解

一、Raft是一种相对简化的分布式一致性算法&#xff0c;它由Diego Ongaro和John Ousterhout于2013年提出。与Paxos相比&#xff0c;Raft的设计目标是使一致性问题更易于理解、实现和部署。 Raft协议核心思想是将一致性问题分解为几个关键组件&#xff0c;包括领导者选举、日志…

概率论的学习和整理21:用EXCEL来做假设检验(未完成草稿)

目录 1 EXCEL可以用来做假设检验 1.1 如何打开 数据分析 和 规划求解 1.2 EXCEL里关于正态分布的准备知识 2 基本的假设检验 2.1 最基本的假设检验&#xff0c;单边的Z检验 2.1 双样本F检验 2.1.1 例题 2.1.2 进行F检验之前需要满足一些假设条件 2.1.3 计算步骤 2.1…

MySQL 数据抽稀 每分钟取一条

假如原始数据为每5秒一个数据&#xff0c;现在想展示为每4分钟一条数据&#xff0c;先按照分钟数把除以4余数为0的行选出来&#xff0c;在按照 年月日 时分&#xff0c;做组内排序&#xff08;窗函数ROW_NUMBER&#xff09;&#xff0c;最后再拿出序号为1的行。 WITH data_01 …

spring学习笔记十

Spring使用注解Annotation定义Bean 1、UserDao接口和实现类 public interface UserDao {void save(); }Component("userDao") public class UserDaoImpl implements UserDao {public void save() {System.out.println("user dao save...");} }2、SpringC…