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. Implement useDocumentTitle with useRef - detailed explanation of useRef and Hook closure
(1) Use Helmet
a custom title
Install react-helmet
and its type declaration files @types/react-helmet
:
npm i react-helmet
npm i -D @types/react-helmet
"react-helmet": "^6.1.0"
"@types/react-helmet": "^6.1.6"
- high probability need
--force
Edit src\unauthenticated-app\index.tsx
(use Helmet
custom header):
...
import {
Helmet } from 'react-helmet'
export const UnauthenticatedApp = () => {
...
return (
<Container>
<Helmet>
<title>请登录或注册以继续</title>
</Helmet>
...
</Container>
);
};
...
Edit src\authenticated-app.tsx
(use Helmet
custom header):
...
import {
Helmet } from 'react-helmet'
export const AuthenticatedApp = () => {
...
return (
<Container>
<Helmet>
<title>项目列表</title>
</Helmet>
...
</Container>
);
};
...
View the effect
(2) Implement useDocumentTitle with useRef
EDIT src\utils\index.ts
(Added useDocumentTitle
):
...
export const useDocumentTitle = (title: string, keepOnUnmount: boolean = true) => {
const oldTitle = document.title
useEffect(() => {
document.title = title
}, [title])
useEffect(() => () => {
if (!keepOnUnmount) {
document.title = oldTitle
}
}, [])
}
keepOnUnmount
default keep existingtitle
will not restore
Modify the system default title public\index.html
:
<title>Jira任务管理系统</title>
Edit src\unauthenticated-app\index.tsx
(use useDocumentTitle
instead Helmet
of custom header):
...
import {
useDocumentTitle } from "utils";
export const UnauthenticatedApp = () => {
...
useDocumentTitle('请登录或注册以继续')
...
};
...
Edit src\authenticated-app.tsx
(use useDocumentTitle
instead Helmet
of custom header):
...
import {
useDocumentTitle } from "utils";
export const AuthenticatedApp = () => {
...
useDocumentTitle('项目列表', false)
...
};
...
View the effect and switch pages
Submit the code to the remote warehouse, and then it’s lunch time (this part of the changed code will be git
cleared later)
edited src\utils\index.ts
by useDocumentTitle
:
...
export const useDocumentTitle = (title: string, keepOnUnmount: boolean = true) => {
const oldTitle = document.title
console.log('渲染时的oldTitle', oldTitle)
useEffect(() => {
document.title = title
}, [title])
useEffect(() => () => {
if (!keepOnUnmount) {
console.log('卸载时的oldTitle', oldTitle)
document.title = oldTitle
}
}, [])
}
Open the login page, clear the console, and click Login to see:
渲染时的oldTitle 请登录或注册以继续
log out to see
渲染时的 oldTitle 项目列表
卸载时的 oldTitle 请登录或注册以继续
Analysis process:
- login:
AuthenticatedApp
preload(willMount
)useDocumentTitle
implement- Save current
oldTitle
Please log in or register to continue - Execute the first
useEffect
changetitle
to list of items (loaddidMount
) - save the second one
useEffect
(callback
pre-pre-uninstall)
- Save current
- logout:
UnauthenticatedApp
preload(willMount
)useDocumentTitle
implement- Store current
oldTitle
project list - Execute the second of the previous
useEffect
page stored before the firstuseEffect
(callback
pre-unloadwillUnmount
)
- Store current
- Restore the page
title
to what was saved at that time (not logged out)oldTitle
Please log in or register to continue (the step of restoring will be executed even ifUnauthenticatedApp
it is not executed )useDocumentTitle
callback
- Execute
useDocumentTitle
the firstuseEffect
changetitle
to the incomingtitle
- …
- Execute
Let's take a deep simulation to explore this process:
New: src\screens\ProjectList\test.tsx
:
import {
useEffect, useState } from "react"
import {
useMount } from "utils"
export const Test = () => {
const [num, setNum] = useState(0)
const add = () => setNum(num + 1)
useMount(() => {
setInterval(() => {
console.log('useMount setInterval', num)
}, 1000)
})
useEffect(() => () => {
console.log(num)
}, [])
return <div>
<button onClick={
add}>add</button>
<p>
number: {
num}
</p>
</div>
}
EDIT src\screens\ProjectList\index.tsx
(using component Test):
...
import {
Test } from "./test";
export const ProjectList = () => {
...
return (
<Container>
<Test/>
...
</Container>
);
};
...
Check the effect, click add
the button to increase randomly, the timer has always been 0, and after logging out, check the console, it is also 0, and it has been changed to "item list" in before, but after the AuthenticatedApp
final oldTitle
logout, it is "Please log in or register to continue" strikingly similar
This is react hook
the classic pit with closures
Next, customize the function to simulate this process:
EDIT: src\screens\ProjectList\test.tsx
:
import {
useEffect, useState } from "react"
import {
useMount } from "utils"
const test = () => {
let num = 0
const effect = () => {
num += 1
const message = `现在的num值${
num}`
return function unmount() {
console.log(message)
}
}
return effect
}
const add = test()
const unmount = add()
add()
add()
unmount() // 按照直觉,add()执行三次,这里应该打印3,但是实际是1
export const Test = () => {
...}
Refresh the page and print 1. Analyze the execution process according to the closure idea:
// 执行 test 返回 effect 函数
const add = test()
// 执行 effect 函数,返回引用了 message1 的 unmount 函数
const unmount = add()
// 执行 effect 函数,返回引用了 message2 的 unmount 函数
add()
// 执行 effect 函数,返回引用了 message3 的 unmount 函数
add()
unmount() // 按照直觉,add()执行三次,这里应该打印3,但是实际是1
understand? Each time the closure is called callback effect()
is unique, there is no relationship between them, and const unmount
only the return value of a certain time is obtained callback effect()
, of course it is also unique. This is the advantage of the closure, and it is also unnoticed. pit
How to avoid it? In fact, the code has already reminded:
React Hook useEffect has a missing dependency: 'num'. Either include it or remove the dependency array.
EDIT: src\screens\ProjectList\test.tsx
(Deprecated useMount
in exchange for both listening useEffect
in and clearing in ):useEffect
num
unmount
Interval
...
export const Test = () => {
...
useEffect(() => {
const id = setInterval(() => {
console.log('useMount setInterval', num)
}, 1000)
return () => clearInterval(id)
}, [num])
useEffect(() => () => {
console.log(num)
}, [num])
...
}
Re-execution: add
after each time, the timer will output the latest num
, the second useEffect
, that is, unmount
output the last timenum
Return project code (see comments for understanding):
export const useDocumentTitle = (title: string, keepOnUnmount: boolean = true) => {
const oldTitle = document.title;
// 这里的 oldTitle 一直都是最新的
console.log('渲染时的oldTitle', oldTitle)
useEffect(() => {
document.title = title;
}, [title]);
useEffect(() => () => {
if (!keepOnUnmount) {
console.log('卸载时的oldTitle', oldTitle)
// 若不指定监听依赖 title,这里读到的就是 旧oldTitle
document.title = oldTitle;
}
}, []);
};
This intentionally or unintentionally uses the characteristics of closures, but it is not friendly. Next, change the way and use useRef
it to achieve useDocumentTitle
:
Modify src\utils\index.ts
:
import {
useEffect, useRef, useState } from "react";
...
export const useDocumentTitle = (title: string, keepOnUnmount: boolean = true) => {
const oldTitle = useRef(document.title).current;
useEffect(() => {
document.title = title;
}, [title]);
useEffect(() => () => {
if (!keepOnUnmount) {
document.title = oldTitle;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};
Check the effect, clean up the test code and submit
Some reference notes are still in draft stage, so stay tuned. . .