随着学习的不断深入,想系统地看看react
的官方文档,于是整理一篇笔记来总结归纳
useState 和 useEffect
1. react 状态管理
react
使用useState
这个hook
作为其基本的状态管理,需要注意的是,react
的状态在更新时,整个组件会被完整地重新渲染,包括以该组件为根的整个组件树;同时,由于hook
函数是默认无状态的,如果不使用useState
或useEffect
等hook
,那么所有的普通变量都会被重新创建一次
2. 无限 re-render
在使用useState
和useEffect
时,要注意不要陷入无限的re-render
,例如:
- 在组件
hook
函数内顶部直接setSsate
:
import { useEffect, useState } from "react"
const Center = () => {
const [res, setRes] = useState(0)
setRes(2)
return (
<div>{ res }</div>
)
}
export default Center
- 使用
useEffect
时不传入依赖参数,同时在effect
的回调中setState
:
import { useEffect, useState } from "react"
const Center = () => {
const [res, setRes] = useState(0)
useEffect(() => {
setRes(1)
})
return (
<div>{ res }</div>
)
}
export default Center
- 在
useEffect
内setState
和useEffect
有相同的state
:
mport { useEffect, useState } from "react"
const Center = () => {
const [res, setRes] = useState(0)
// 设置的 state 和 依赖的 state 是相同的
useEffect(() => {
setRes(1)
}, [res])
return (
<div>{ res }</div>
)
}
export default Center
这是因为每次setState
都会导致组件全量重新渲染,而每次全量渲染的时候又会执行setState
,从而陷入死循环:
3. useEffect 执行机制
在useEffect
中可以return
一个cleanup
函数,每当依赖项变更后,先使用旧的state、props
来运行cleanup
函数,然后再使用新的state、props
运行setup
函数
通过这个机制,在useEffect
内部的数据请求可以做一个 "竞态" 优化,实际上就是通过闭包来忽略之前的请求结果,以避免先发出的请求后获取到结果而将后发出的请求所获取到的结果覆盖,官方文档的示例:使用 Effect 同步 – React 中文文档
4. useEffect 的依赖项
useEffect
的依赖列表不一定必须要响应式值,它可以是任意的在组件函数内部声明的变量或函数,只要在每一轮的渲染中react
用Object.is
来比较,发现前后的值不一样的话,就会触发这个useEffect
的执行
在下面这个例子中,每次点击按钮更新state
都会触发普通变量doubleNumber
的更新,从而触发useEffect
执行:
import { useEffect, useState } from "react"
const EffectLearn = () => {
const [number, setNumber] = useState(0)
const doubleNumber = number * 2
useEffect(() => {
console.log("doubleNumber", doubleNumber)
}, [doubleNumber])
return (
<>
<button onClick={() => setNumber(number => number + 1)}>点击加一:{number}</button>
double: {doubleNumber}
</>
)
}
export default EffectLearn
另一种状态 -- useRef
使用ref
后,react
会在后续的组件渲染中保存该值,不会像普通变量那样因组件重新渲染而重置;它与state
的不同点是ref
不会因修改而触发组件的重新渲染
ref
有两个用途:
用于引用并保存一个当前值
用于引用
dom
或组件元素,可以获取到元素的实例
对于ref
的读取与写入,官方文档有如下说明:
组件 props
关于props
的基本使用,大家肯定都不陌生,那么这里就只说一点:props
不属于state
,但它仍然可以被useEffect
监听
实际上,只要是一个变量 ( 包括普通变量 ),就可以被useEffect
监听;但是useEffect
的定义是要在组件重新渲染后执行的,所以又必须通过修改state
来触发组件的重新渲染,重新渲染后,useEffect
会比较其依赖列表中上一次渲染时的值和本次渲染时的值是否相同,来判断是否要执行effect
回调
createContext 和 useContext
这两个api
有点类似与vue
的provide、inject
,但是createContext
和useContext
在获取数据时并不局限于组件树中的层级关系,而是可以将context
抽离出来
1. 在组件树中提供数据
使用createContext
创建一个context
,然后用context
的provider
来向其下的组件树提供该context
value
就是你要通过这个provider
向组件树提供的context
值
import { createContext } from 'react'
const ThemeContext = createContext('light')
function App() {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={theme}>
<Page />
</ThemeContext.Provider>
);
}
2. 将 context 抽离到一个文件中并导出使用
在子组件中使用useContext
来获取某个context
,如果在父组件中的组件树都没有使用provider
来提供值,那么就会使用createContext
时所传入的初始值 ( 默认值 ),这个特性就使得了context
可以被抽离出来到一个文件中
这个特性一般与mobx
结合使用导出一个store
,下面是使用示例
- 创建一个
store
:
import { makeAutoObservable } from 'mobx'
// 定义一个类,作为 store
class Counter {
// 属性是 store 要存储的数据
count = 0
constructor() {
makeAutoObservable(this)
}
// 方法就是修改 state 的 action
increase() {
this.count += 1
}
decrease() {
this.count -= 1
}
}
export default Counter
- 抽离到一个统一的
store
中导出使用:
import Counter from "./counter"
import { createContext, useContext } from "react"
const store = {
counterStore: new Counter()
}
const StoreContext = createContext(store)
// 自定义 hook,结合 context,用于获取 store
const useStore = () => useContext(StoreContext)
export default useStore
- 在组件中使用
store
:
import useStore from "@/store"
import { observable } from "mobx-react"
function App() {
const { counterStore } = useStore()
return (
<div>{counterStore.count}</div>
)
}
// 用 observable 包裹组件,响应式监听 store
export default observable(App)
使用 useCallback 缓存函数
这是一个性能优化hook
,用于将传入回调函数在组件首次渲染后缓存起来,即后续的重复渲染不会创建新的函数了
1. 在 useCallback 中更新 state
在一个函数中更新state
是非常常见的,但如果你将他包裹到useCallback
中,就有一些问题需要注意了,看下面的例子:
在
handleSetNumberCommon
函数中,你多次点击后会使得number
逐渐增加在
handleSetNumberCallback
函数中,你多次点击后number
总是为 1
这就很好地阐述了useCallback
的作用:将一个函数缓存起来;当你将一个函数缓存起来时,只有在组件首次渲染时会创建并返回这个函数,所以组件首次渲染时number
为 0,那么number + 1
就永远为 1 ,由于函数中使用的state
只是一个值,所以它也永远不会改变
import { useCallback, useState } from "react"
const UseCallbackLearn = () => {
const [number, setNumber] = useState(0)
const doubleNumber = number * 2
const handleSetNumberCommon = () => {
setNumber(number + 1)
}
const handleSetNumberCallback = useCallback(() => {
setNumber(number + 1)
}, [])
return (
<>
<button onClick={handleSetNumberCommon}>不使用callback加一</button>
<button onClick={handleSetNumberCallback}>使用callback加一</button>
<div>number: {number}</div>
<div>double: {doubleNumber}</div>
</>
)
}
export default UseCallbackLearn
要处理上述的bug
,我们就要为useCallback
声明依赖项,如下:
import { useCallback, useState } from "react"
const UseCallbackLearn = () => {
const [number, setNumber] = useState(0)
const doubleNumber = number * 2
// 声明依赖项,在 number 变化时重新创建这个函数
const handleSetNumberCallback = useCallback(() => {
setNumber(number + 1)
}, [number])
return (
<>
<button onClick={handleSetNumberCallback}>使用callback加一</button>
<div>number: {number}</div>
<div>double: {doubleNumber}</div>
</>
)
}
export default UseCallbackLearn
但是这种做法在react
官方文档中并不推荐,我们可以将其改写为如下形式:
import { useCallback, useState } from "react"
const UseCallbackLearn = () => {
const [number, setNumber] = useState(0)
const doubleNumber = number * 2
// 向 set 函数传入一个更新函数,取消声明依赖项
const handleSetNumberCallback = useCallback(() => {
setNumber(number => number + 1)
}, [])
return (
<>
<button onClick={handleSetNumberCallback}>使用callback加一</button>
<div>number: {number}</div>
<div>double: {doubleNumber}</div>
</>
)
}
export default UseCallbackLearn
lazy() 函数
lazy
函数接收一个回调函数,该回调在你真正使用导入的组件之前都不会执行,该回调执行了之后,组件的引入就会被缓存,之后使用该组件时,都不会重新引入了,除非手动刷新应用
lazy
函数用于动态地引入组件,可以在打包时进行代码分割;除此之外,lazy
还可以在组件初次挂载完成前触发Suspense
的fallback
备用方案
lazy
能够触发Suspense
组件的fallback
的原因是它返回一个promise
,在组件引入完成之前,promise
会一直被阻塞
Suspense 组件
在阅读react
文档的 "参考" 的时候看到了Suspense
组件,于是看了看官方文档的例子,发现每当组件重新获取数据(promise
)时,都会触发Suspense
的fallback
,但是文档中又提到了Suspense
无法检测useEffect
中的数据请求,那么它是如何触发Suspense
的?我查了一下之后发现,应该是和react-cache
结合使用的
1. Suspense fallback 的触发条件
官方文档对于Suspense fallback
的触发条件的说明如下:
所以说,在正常情况下,使用CSR react
时,Suspense
的作用就是组件懒加载时显示fallback
组件,在useEffect
中进行异步数据请求时,Suspense
组件并不起作用
实际上,Suspense
组件会捕获它的children
内抛出的任意东西,如果抛出一个promise
,则Suspense
会显示fallback
组件,当这个promise
被resolve
后,它的children
组件就会重新挂载
2. 结合 react-cache 使用
那么如果不在useEffect
中请求数据呢?我们可以使用react-cache
;具体的使用方法可以参考这篇文章:展望 react-cache,一个 React 官方的处理数据副作用方案 - 掘金 (juejin.cn)
现在我们手动实现一个简陋版的react-cache
来看一下:
function wrapPromise(promise) {
let status = 0;
let result;
// 调用promise,并在回调中更改加工函数中的状态
const callPromise = promise.then(
(res) => {
status = 1;
result = res;
},
(err) => {
status = -1;
result = err;
}
);
// 返回一个对象,只需要提供read方法
return {
read() {
switch (status) {
case 0:
throw callPromise;
case 1:
return result;
case -1:
throw result;
}
},
};
}
export default wrapPromise
然后我们在组件文件的顶部调用wrapPromise
函数,并在组件hook
函数内调用read()
方法获取数据:
import { useEffect, useState } from "react"
import wrapPromise from "../../utils/throw-promise"
const promise = () => {
return new Promise<number>((resolve) => {
setTimeout(() => resolve(1), 2000)
})
}
// 在文件顶部调用加工函数
const data = wrapPromise(promise())
const Center = () => {
// 在组件 hook 内读取数据
const res = data.read()
return (
<>
<div>{ res }</div>
</>
)
}
export default Center
但是这样有一个坏处就是你在组件文件顶部调用wrapPromise
,那么获取到的数据是固定的,即使组件后面会重新挂载;所以官方文档目前也是不推荐在实际开发中使用
我还看了一下这个包5年前就已经发布了,到现在都还没有什么大的进展,估计也没啥用了:react-cache - npm (npmjs.com)
那么,Suspense
在客户端组件下就只有lazy
能触发fallback
吗?其实,还有一个非常流行的数据请求管理库--react-query
,现在叫tanstack-query
,可以在获取数据阶段触发Suspense
组件的fallback
,但是我打算后面再单独开一篇博客来讲一下,这里就仅仅只是引出这个库,不做过多介绍