学习内容来源: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 应用:实现项目列表
- 三、 TS 应用:JS神助攻 - 强类型
- 四、 JWT、用户认证与异步请求(上)
- 四、 JWT、用户认证与异步请求(下)
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(上)
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(下)
- 六、用户体验优化 - 加载中和错误状态处理(上)
- 六、用户体验优化 - 加载中和错误状态处理(中)
- 六、用户体验优化 - 加载中和错误状态处理(下)
- 七、Hook,路由,与 URL 状态管理(上)
- 七、Hook,路由,与 URL 状态管理(中)
- 七、Hook,路由,与 URL 状态管理(下)
上一节最后的 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 } = props
return <Select
value={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)
修改 src\screens\ProjectList\components\List.tsx
(将 Project
中的 id
和 personId
类型统一改为 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>
<UserSelect
defaultOptionName="负责人"
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 (
<Select
value={options?.length ? toNumber(value) : 0}
{...}
>...</Select>
);
};
...
查看页面效果,完美!
为每个项目新增一个收藏标记
新建组件 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 <Rate
count={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
export const List = ({ users, ...props }: ListProps) => {
const { mutate } = useEditProject()
// 函数式编程 柯里化
const starProject = (id: number) => (star: boolean) => mutate({id, star})
return (
<Table
pagination={false}
columns={[
{
title: <Star checked={true} disabled={true}/>,
render: (val, record) =>
<Star
checked={record.star}
// stared => starProject(record.id)(stared)
onCheckedChange={starProject(record.id)}
/>
},
...
]}
{...props}
></Table>
);
};
- 在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
- 柯里化(currying)又称部分求值。一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
查看页面,点击标记一个,但是没有反应,控制台 Network 中有网络请求,刷新页面再看,数据已经更新了,这个问题后续解决
部分引用笔记还在草稿阶段,敬请期待。。。