Article Directory
-
- 1. Project launch: project initialization and configuration
- 2. React and Hook application: implement the project list
- 3. TS Application: JS God Assist - Strong Type
- 4. JWT, user authentication and asynchronous request
- 5. CSS is actually very simple - add styles with CSS-in-JS
- 6. User experience optimization - loading and error state handling
- 7. Hook, routing, and URL state management
Source of learning content: React + React Hook + TS Best Practice - MOOC
Compared with the original tutorial, I used the latest version at the beginning of my study (2023.03):
item | Version |
---|---|
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 |
The specific configuration, operation and content will be different, and the "pit" will also be different. . .
1. Project launch: project initialization and configuration
2. React and Hook application: implement the project list
3. TS Application: JS God Assist - Strong Type
4. JWT, user authentication and asynchronous request
5. CSS is actually very simple - add styles with CSS-in-JS
6. User experience optimization - loading and error state handling
7. Hook, routing, and URL state management
1+2.
3. Add project list and project details routing
Install react-router react-router-dom
# 可能会需要 --force
npm i react-router react-router-dom history
The latest version
- “react-router”: “^6.11.2”
- “react-router-dom”: “^6.11.2”
- “history”: “^5.3.0”
New project details page src\screens\ProjectDetail\index.tsx
:
export const ProjectDetail = () => {
return <h1>ProjectDetail</h1>
};
Modification src\authenticated-app.tsx
(extract PageHeader, introduce and use routing-related components):
...
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>
}
...
Modify the project list page src\screens\ProjectList\components\List.tsx
(introduce the routing component 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>
);
};
View the effect:
- Visit the project list page: http://localhost:3000/projects
- Click on the project name to visit the project details page
4. Add kanban and task group routing
Create a new Kanban page src\screens\ViewBoard\index.tsx
:
export const ViewBoard = () => {
return <h1>ViewBoard</h1>
};
New task group page src\screens\TaskGroup\index.tsx
:
export const TaskGroup = () => {
return <h1>TaskGroup</h1>
};
Modify the project details page src\screens\ProjectDetail\index.tsx
(redirect to Kanban by default, and you can also switch Kanban and task groups freely):
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>
};
/
Represents the root route. If you want to specify an additional path after the current route, you only need to write the path without adding/
- using
Navigate
redirected can be writtenRoute
aspath='/'
index
- An error is reported during the use of Navigate:
[Navigate] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>
- The code of the blogger is the code that can already run normally. For other ideas, please refer to: [Must-see, important reminder] The version of React Router has been updated-MOOC
Similarly, after logging in, you need to jump to the default path /projects
, and click PageHeader
to logo
automatically jump to the root path
Modification src\utils\index.ts
(addition resetRoute
):
...
export const resetRoute = () => window.location.href = window.location.origin
Modify 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. Preliminary implementation of useUrlQueryParam to manage URL parameter status
New 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
}
There will be some problems with this part of the type system, other visible annotations
- as const is a type of type assertion that avoids using its default type inference behavior, resulting in a wider or more general type.
- for example:
let a = ['string', 123, true]
,a
will be inferred as(string | number | boolean)[]
let a = ['string', 123, true, {}]
,a
but it will be inferred that{}[]
as const
After addinglet a = ["string", 123, true, {}] as const
,a
it will be inferred asreadonly ["string", 123, true, {}]
Modification src\screens\ProjectList\index.tsx
(use useUrlQueryParam
from url
to get parameters):
...
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;
`;
So far, the desired function has been realized, but the page is continuously rendered in a loop. . . Let's find out why
6. Use useMemo to solve the dependency cycle problem-Detailed explanation of Hook's dependency problem
install why-did-you-render
:
# 可能会需要 --force
npm i @welldone-software/why-did-you-render
“@welldone-software/why-did-you-render”: “^7.0.1”
New 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
Introduce globally on the first line:
import './wdyr'
src\screens\ProjectList\index.tsx
Add to the component that needs to track the problem :
ProjectList.whyDidYouRenger = true
Or the question of which component is not sure which component can be set src\wdyr.ts
totrackAllPureComponents
true
I tried to add it alone, but it didn't work, so I used the default configuration, global tracking
The running result is shown in the figure:
Re-rendered because of hook changes:
e-rendered because of props changes:"
different objects that are equal by value.
It is not difficult to find that the values of two different objects are equal, and they are monitored in useEffect, and then there is an infinite loop. . .
That means that the two objects are not the same (the reference address is different), that is to say, every time useEffect (useDebounce) is a newly created object
Then trace the source to find useUrlQueryParam ( src\utils\url.ts
).
Aha, here is the problem, searchParams
it is new every time, how to solve it? Use 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;
};
Basic types and component states can be placed in dependencies; non-component state reference types (objects, arrays, methods) cannot.
When the componentstate
value (defined byuseState
) is placed in the dependency list ofuseEffect
or , the value will be triggereduseMemo
because only the corresponding is recognized changes, so it doesn't cause an infinite loop.setState
state
View the page after modification, the infinite loop is gone
The bug is gone, you can set src\wdyr.ts
middle to , or in the component:trackAllPureComponents
false
ProjectList.whyDidYouRenger = false
Some reference notes are still in draft stage, so stay tuned. . .