学习内容来源: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 添加样式(下)
- 六、用户体验优化 - 加载中和错误状态处理(上)
- 六、用户体验优化 - 加载中和错误状态处理(中)
- 六、用户体验优化 - 加载中和错误状态处理(下)
Helmet
自定义标题安装 react-helmet
和它的类型声明文件 @types/react-helmet
:
npm i react-helmet
npm i -D @types/react-helmet
"react-helmet": "^6.1.0"
"@types/react-helmet": "^6.1.6"
- 大概率需要
--force
修改 src\unauthenticated-app\index.tsx
(使用 Helmet
自定义标题):
...
import { Helmet } from 'react-helmet'
export const UnauthenticatedApp = () => {
...
return (
<Container>
<Helmet>
<title>请登录或注册以继续</title>
</Helmet>
...
</Container>
);
};
...
修改 src\authenticated-app.tsx
(使用 Helmet
自定义标题):
...
import { Helmet } from 'react-helmet'
export const AuthenticatedApp = () => {
...
return (
<Container>
<Helmet>
<title>项目列表</title>
</Helmet>
...
</Container>
);
};
...
查看效果
编辑 src\utils\index.ts
(新增 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
默认保持现有title
不会还原
修改系统默认标题 public\index.html
:
<title>Jira任务管理系统title>
修改 src\unauthenticated-app\index.tsx
(使用 useDocumentTitle
代替 Helmet
自定义标题):
...
import { useDocumentTitle } from "utils";
export const UnauthenticatedApp = () => {
...
useDocumentTitle('请登录或注册以继续')
...
};
...
修改 src\authenticated-app.tsx
(使用 useDocumentTitle
代替 Helmet
自定义标题):
...
import { useDocumentTitle } from "utils";
export const AuthenticatedApp = () => {
...
useDocumentTitle('项目列表', false)
...
};
...
查看效果,并切换页面
提交代码到远程仓库,接下来是下饭时间(这部分更改的代码后续会通过 git
清掉)
编辑 src\utils\index.ts
的 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
}
}, [])
}
打开登录页,清理控制台,点击登录可看到:
渲染时的oldTitle 请登录或注册以继续
登出,可看到
渲染时的 oldTitle 项目列表
卸载时的 oldTitle 请登录或注册以继续
分析过程:
AuthenticatedApp
预加载(willMount
)
useDocumentTitle
执行
oldTitle
请登录或注册以继续useEffect
更改 title
为 项目列表(加载 didMount
)useEffect
的 callback
存起来(预预卸载)UnauthenticatedApp
预加载(willMount
)
useDocumentTitle
执行
oldTitle
项目列表useEffect
前执行之前存储的 上个页面的 第二个 useEffect
的 callback
(预卸载 willUnmount
)title
还原为当时(未登出)存起来的 oldTitle
请登录或注册以继续(还原这一步即使 UnauthenticatedApp
没有执行 useDocumentTitle
也会执行 callback
)
useDocumentTitle
第一个 useEffect
更改 title
为 传入的 title
接下来深度模拟探究一下这一过程:
新建: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>
}
编辑 src\screens\ProjectList\index.tsx
(使用组件 Test):
...
import { Test } from "./test";
export const ProjectList = () => {
...
return (
<Container>
<Test/>
...
</Container>
);
};
...
查看效果,点击 add
按钮随机增加,定时器中一直是0,登出后查看控制台,也是 0 ,和之前明明在 AuthenticatedApp
中 oldTitle
已经改为“项目列表”,最终登出后却是“请登录或注册以继续”惊人的相似
这就是 react hook
与 闭包 经典的坑
接下来自定义函数来模拟这个过程:
编辑: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 = () => {...}
刷新页面,打印的是1,按闭包思路分析执行过程:
// 执行 test 返回 effect 函数
const add = test()
// 执行 effect 函数,返回引用了 message1 的 unmount 函数
const unmount = add()
// 执行 effect 函数,返回引用了 message2 的 unmount 函数
add()
// 执行 effect 函数,返回引用了 message3 的 unmount 函数
add()
unmount() // 按照直觉,add()执行三次,这里应该打印3,但是实际是1
懂了吗?每次调用闭包中 callback effect()
都是唯一的,之间并无关联,而 const unmount
拿到的只是某一次 callback effect()
的返回值,当然也是独一无二的,这即是闭包的优势,也是注意不到时,闭包的坑
如何避免呢?其实代码中已经提醒了:
React Hook useEffect has a missing dependency: 'num'. Either include it or remove the dependency array.
编辑:src\screens\ProjectList\test.tsx
(弃用 useMount
换回 useEffect
,并在 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])
...
}
重新执行:每次 add
后,定时器都会 输出最新 num
, 第二个 useEffect
,即 unmount
输出上一次的 num
回归项目代码(理解看注释):
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;
}
}, []);
};
这样有意无意地使用了闭包的特性,但是并不友好。接下来换个方式,使用 useRef
实现 useDocumentTitle
:
修改 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
}, []);
};
- Hook API 索引 – React
查看效果,清理测试代码并提交
部分引用笔记还在草稿阶段,敬请期待。。。