文章目录
- 一、项目起航:项目初始化与配置
- 二、React 与 Hook 应用:实现项目列表
- 三、TS 应用:JS神助攻 - 强类型
- 四、JWT、用户认证与异步请求
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
- 六、用户体验优化 - 加载中和错误状态处理
- 七、Hook,路由,与 URL 状态管理
- 1+2.
- 3.添加项目列表和项目详情路由
- 4. 添加看板和任务组路由
- 5. 初步实现 useUrlQueryParam 管理 URL 参数状态
- 6.用useMemo解决依赖循环问题 - Hook的依赖问题详解
学习内容来源: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 状态管理
1+2.
- 七、Hook,路由,与 URL 状态管理(上)
3.添加项目列表和项目详情路由
Home v6.11.2 | React Router
安装 react-router react-router-dom
# 可能会需要 --force
npm i react-router react-router-dom history
目前最新版
- “react-router”: “^6.11.2”
- “react-router-dom”: “^6.11.2”
- “history”: “^5.3.0”
新建项目详情页 src\screens\ProjectDetail\index.tsx
:
export const ProjectDetail = () => {
return <h1>ProjectDetail</h1>
};
修改 src\authenticated-app.tsx
(将 PageHeader 提取出来,引入并使用 路由相关组件):
...
import { Routes, Route } from "react-router";
import { BrowserRouter as Router } from "react-router-dom";
import { ProjectDetail } from "screens/ProjectDetail";
export const AuthenticatedApp = () => {
useDocumentTitle("项目列表", false);
return (
<Container>
<PageHeader/>
<Main>
<Router>
<Routes>
<Route path={'/projects'} element={<ProjectList/>}/>
<Route path={'/projects/:projectId/*'} element={<ProjectDetail/>}/>
</Routes>
</Router>
</Main>
</Container>
);
};
const PageHeader = () => {
const { logout, user } = useAuth();
const items: MenuProps["items"] = [
{
key: 1,
label: "登出",
onClick: logout,
},
];
return <Header between={true}>
<HeaderLeft gap={true}>
<SoftwareLogo width="18rem" color="rgb(38,132,255)" />
<h2>项目</h2>
<h2>用户</h2>
</HeaderLeft>
<HeaderRight>
<Dropdown menu={{ items }}>
<Button type="link" onClick={(e) => e.preventDefault()}>
Hi, {user?.name}
</Button>
</Dropdown>
</HeaderRight>
</Header>
}
...
修改项目列表页 src\screens\ProjectList\components\List.tsx
(引入 路由组件 Link):
...
// react-router 和 react-router-dom 的关系,类似于 react 和 react-dom/react-native/react-vr...
import { Link } from 'react-router-dom'
// TODO 把所有 id personId 都改为 number 类型
...
// type PropsType = Omit<ListProps, 'users'>
export const List = ({ users, ...props }: ListProps) => {
return (
<Table
pagination={false}
columns={[
{
title: "名称",
dataIndex: "name",
sorter: (a, b) => a.name.localeCompare(b.name),
render: (text, record) => <Link to={String(record.id)}>{text}</Link>
},
...
]}
{...props}
></Table>
);
};
查看效果:
- 访问项目列表页:http://localhost:3000/projects
- 点击项目名称访问项目详情页
4. 添加看板和任务组路由
新建看板页 src\screens\ViewBoard\index.tsx
:
export const ViewBoard = () => {
return <h1>ViewBoard</h1>
};
新建任务组页 src\screens\TaskGroup\index.tsx
:
export const TaskGroup = () => {
return <h1>TaskGroup</h1>
};
修改项目详情页 src\screens\ProjectDetail\index.tsx
(默认重定向到看板,也可自由切换看板和任务组):
import { Link, Navigate } from "react-router-dom";
import { Route, Routes } from "react-router";
import { TaskGroup } from "screens/TaskGroup";
import { ViewBoard } from "screens/ViewBoard";
export const ProjectDetail = () => {
return <div>
<h1>ProjectDetail</h1>
<Link to='viewboard'>看板</Link>
<Link to='taskgroup'>任务组</Link>
<Routes>
<Route path='/viewboard' element={<ViewBoard/>}/>
<Route path='/taskgroup' element={<TaskGroup/>}/>
<Route index element={<Navigate to='viewboard'/>}/>
</Routes>
</div>
};
/
代表根路由,若要在当前路由后指定追加路径,只需要写路径即可不需要加/
- 使用
Navigate
需要重定向的Route
的path='/'
可以写成index
- Navigate 使用过程中报错:
[Navigate] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>
- 博主代码是已经可以正常运行的代码,其他思路可参考:【必看,重要提示】React Router 版本已更新-慕课网
同理登录后需要默认跳转到 /projects
,且点击 PageHeader
的 logo
自动跳转根路径
修改 src\utils\index.ts
(新增 resetRoute
):
...
export const resetRoute = () => window.location.href = window.location.origin
修改 src\authenticated-app.tsx
:
...
import { resetRoute, useDocumentTitle } from "utils";
...
import { Navigate, BrowserRouter as Router } from "react-router-dom";
...
export const AuthenticatedApp = () => {
...
return (
<Container>
<PageHeader />
<Main>
<Router>
<Routes>
...
<Route index element={<Navigate to='projects'/>}/>
</Routes>
</Router>
</Main>
</Container>
);
};
const PageHeader = () => {
...
return (
<Header between={true}>
<HeaderLeft gap={true}>
<Button type='link' onClick={resetRoute}>
<SoftwareLogo width="18rem" color="rgb(38,132,255)" />
</Button>
...
</HeaderLeft>
...
</Header>
);
};
...
5. 初步实现 useUrlQueryParam 管理 URL 参数状态
新建 src\utils\url.ts
:
import { useSearchParams } from "react-router-dom";
/**
* 返回页面 url 中,指定键的参数值
* @param keys
* - keys 的类型“{ [x: string]: string; }”缺少类型“{ name: string; personId: string; }”中的以下属性: name, personId
* - 由于数据的下游要求指定的 key name 且是 string 类型,因此 keys 需要设定为泛型以做兼容
* - 计算属性名的类型必须为 "string"、"number"、"symbol" 或 "any"。泛型 K 需要 `extends string` 约束
* @returns
*/
export const useUrlQueryParam = <K extends string>(keys: K[]) => {
const [ searchParams, setSearchParams ] = useSearchParams()
return [
keys.reduce((prev, key) => {
// searchParams.get 可能会返回 null,需要预设值来兼容
return {...prev, [key]: searchParams.get(key) || ''}
// 初始值会对类型造成影响,需要手动指定
}, {} as { [key in K]: string }),
setSearchParams
] as const
}
- URLSearchParams - Web API 接口参考 | MDN
这部分类型系统会有些问题,其他可见注释
- as const 是类型断言的一种,避免使用其默认类型推断行为,导致更广泛或更一般的类型。
- 比如:
let a = ['string', 123, true]
,a
会被推断为(string | number | boolean)[]
let a = ['string', 123, true, {}]
,a
却会被推断为{}[]
- 加上
as const
后let a = ["string", 123, true, {}] as const
,a
会被推断为readonly ["string", 123, true, {}]
修改 src\screens\ProjectList\index.tsx
(使用 useUrlQueryParam
从 url
中拿参数):
...
import { useUrlQueryParam } from "utils/url";
export const ProjectList = () => {
const [, setParam] = useState({
name: "",
personId: "",
});
const [param] = useUrlQueryParam(['name', 'personId'])
...
};
const Container = styled.div`
padding: 3.2rem;
`;
至此,想要的功能实现了,但是页面在持续循环渲染。。。下面来找下原因
6.用useMemo解决依赖循环问题 - Hook的依赖问题详解
安装 why-did-you-render
:
# 可能会需要 --force
npm i @welldone-software/why-did-you-render
“@welldone-software/why-did-you-render”: “^7.0.1”
新建 src\wdyr.ts
:
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
// 跟踪所有组件
trackAllPureComponents: false,
});
}
在 src\index.tsx
第一行全局引入:
import './wdyr'
在需要追踪问题的组件 src\screens\ProjectList\index.tsx
中加入:
ProjectList.whyDidYouRenger = true
或者不确定哪个组件的问题可以将 src\wdyr.ts
中 trackAllPureComponents
设为 true
我尝试单独加不管用,所以就使用了默认配置,全局追踪
运行结果如图:
Re-rendered because of hook changes:
e-rendered because of props changes:"
different objects that are equal by value.
不难发现,两个不同的 objects 值相等,而且在 useEffect 中监听了,然后就死循环了。。。
那说明两个 objects 不是同一个(引用地址不同),也就是说每次给 useEffect(useDebounce) 的都是新创建的对象
然后追根溯源找到 useUrlQueryParam (src\utils\url.ts
)。
啊哈,问题就在这里了,每次 searchParams
都是新的,如何解决呢?使用 useMemo
import { useMemo } from "react";
...
export const useUrlQueryParam = <K extends string>(keys: K[]) => {
const [searchParams, setSearchParams] = useSearchParams();
return [
useMemo(
() => keys.reduce((prev, key) => {
// searchParams.get 可能会返回 null,需要预设值来兼容
return { ...prev, [key]: searchParams.get(key) || "" };
// 初始值会对类型造成影响,需要手动指定
}, {} as { [key in K]: string }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[searchParams]
),
setSearchParams,
] as const;
};
Hook API 索引 – React
基本类型,组件状态可以放到依赖里;非组件状态的引用类型(对象,数组,方法)不可以
当组件state
值(通过useState
定义)放在useEffect
或useMemo
的依赖列表中时,由于只承认对应setState
才会触发state
值的改变,因此不会造成无限循环。
修改后查看页面,无限循环没有了
bug 没了,可以将 src\wdyr.ts
中 trackAllPureComponents
设为 false
,或是组件中:
ProjectList.whyDidYouRenger = false
部分引用笔记还在草稿阶段,敬请期待。。。