大家好,我是森木。本文将从平时开发常用的 Hook 入手慢慢将每个 Hook 的细节扒开。宗旨是让大家轻松阅读,收获满满。
前言
数据的处理涉及到我们开发日常的方方面面,在习惯了某些方法后可能容易忽视其他细节;不过,接下来我同大家一起来回顾下 React 最常用的几个 Hook。废话少说,直接开整。
useState
用法:
const [value, setValue] = useState(initValue)
/**
* 更新 value
*/
setValue(xx);
// 这里还是原来 value
console.log(value);
/**
* 修改后
*/
setValue((prevValue) => {
// 返回的就是最新的值
return prevValue + 1
});
// 这里拿到的就是最新的 value
console.log(value);
useState
我这里就谈两点细节:
- 惰性初始化
- 重置数据
惰性初始化
基本使用就不再赘述了,这里强调下 initValue
,它支持所有类型的数据,但是函数类型会有特殊的逻辑,它有个优雅的名字——惰性状态函数。那么它有什么不同呢?
- React 会将传入的函数保存下来,并在初始化渲染时执行
- 修改状态通过
setState
它最大的用处就是性能优化,想象一下有一个需要通过复杂“计算”的初始值,如果你不想在每次重新渲染都去“计算”的话这种方式就是最好的方式~
加餐:既然都是为了不在重新渲染后重复执行那么使用
useEffect
的空依赖数组岂不是一样?(这个放在useEffect
章节详细讲)
重置数据
重置数据小技巧:给组件设置 key
,并且传递 state
值给 key
,在需要重置数据时改变 key
值。
useEffect
用法:
useEffect(() => {
// setup
return () => {
// cleanup
}
}, [/*依赖项*/])
关于 useEffect
这里讲三点细节:
- 依赖项
- 数据请求
- Effect 生命周期
关于依赖项
- 不填(指不写
useEffect
第二个参数),那么每次重新渲染都会执行 Effect - 为空数组的,只有在
mount
阶段执行setup
逻辑,unmount
阶段执行cleanup
逻辑 - 填写响应式依赖,在
mount
阶段执行setup
逻辑,每次响应式值改变先执行cleanup
逻辑,后执行setup
逻辑,unmount
阶段执行cleanup
逻辑
数据请求
// 切换 person state 来请求最新数据
const [person, setPerson] = useState('senmu')
const [list, setList] = useState()
// 使用 .then 方式
useEffect(() => {
let flag = false
fetchData(person).then(res => {
if (!flag) {
setList(res)
}
})
return () => {
flag = true
}
}, [person])
// 使用 async await 方式
useEffect(() => {
async function getData() {
const res = await fetchData(person)
if (!flag) {
setList(res)
}
}
let flag = false
getData()
return () => {
flag = true
}
}, [person])
很多人看到上面的代码表示不屑(我天天写数据请求难道还用你教我 🙄)。其实这里面有一点细节需要我们注意:
useEffect
的第一个参数不允许是Promise
,如果要写async
需要在内部单独定义函数;- 关于为什么要定义
flag
变量,是因为假如在请求还未完成时执行了意外的操作(例如组件卸载),那么数据就不该被设置到state
中。
Effect 的生命周期
mount
阶段:
- 初始化变量
- 渲染 DOM(
render
) - 执行
Effect setup
逻辑
update
阶段:
- 渲染 DOM(
render
) - 执行
Effect cleanup
逻辑 - 执行
Effect setup
逻辑
unmount
阶段:
- 执行
Effect cleanup
逻辑
下面是代码实例,可以使用控制台查看:
明白了上面的生命周期回头思考下在 useState
中留下来的疑问,我们可以发现 useEffect
的执行时机是在渲染之后,而惰性初始化却是在初始化渲染时执行,这就意味着如果在渲染之后执行赋值操作会造成一次重新渲染,并且这样处理也不够优雅🤵。
useLayoutEffect
只针对客户端
用法/写法上与 useEffect
无异,只不过需要注意它比 useEffect
生命周期更早并且会阻塞浏览器重绘,这也意味着它的性能较差,所以一般建议使用 useEffect
,除非特殊场景。
比如鼠标移入弹出提示框功能,需要通过获取位置信息来生成提示框信息,下面可以看看具体对比:
useInsertionEffect
只针对客户端
同样用法/写法上与上述的 Effect 无异,只不过需要注意它比 useLayoutEffect
生命周期更早,设计初心也是为了解决使用 JS 插入 CSS 由于时机问题引发的性能问题。实例:
import { useState, useInsertionEffect } from 'react'
function MyComponent() {
const [color, setColor] = useState('blue');
useInsertionEffect(() => {
// 假设 insertStyles 是一个将 CSS 注入到 DOM 的函数
insertStyles(`.my-component { color: ${color}; }`);
}, [color]); // 依赖项数组中包含 color,确保颜色变化时样式可以更新
return <div className="my-component">Hello, World!</div>;
}
useContext
提到
useContext
不可避免的就是 React 的Context
整个设计
Context 特性
- 跨组件传递数据
- 避免
Props
逐层传递 - 动态性—可以结合
state
和reducer
动态更改其值 - 默认值—如果上下文的
Provider
未提供值,那么便会采用创建Context
时传递的默认值 - 多个
Context
—可以使用多个Provider
嵌套以支持多种Context
,每个Context
管理不同的数据
注意事项:
- 使用
Context
的组件由于状态与组件的绑定导致组件的复用性大大降低 - 由于重新渲染引发的对于性能的考量
关键步骤
- 使用
const ThemeContext = createContext(defaultValue)
创建Context
- 使用
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
包裹子组件 - 在被 2 中包裹的任意子组件中使用
useContext(ThemeContext)
来获取状态值
以上三点缺一不可~
从设计上来讲,Context
可以包裹多层多次组件,那么使用 useContext()
拿到的值就是距离最近的 Provider
提供的值。
再啰嗦一句,Context
虽好,但是在使用之前一定要仔细考量是否真的需要使用 Context
。
useReducer
用法:
const [state, dispatch] = useReducer(reducer, initValue, initFn?)
关于它的用法上有一点值得注意,那就是如果需要传递复杂的或者需要计算得到的初始化值,你可以把 initValue
当作 initFn
的参数,在 initFn
中返回通过计算得到的初始值,这样做的好处就是可以避免重复渲染导致不必要的性能开销。
关于它与 useState
,useReducer
更像是需要提前进行技术设计才去使用的功能,而 useState
更偏向是此时此刻我需要这个状态我就会使用,这也导致它们有各自不同的结果和影响。
它可以和 useContext
结合,封装更加强大的功能,并使交互与状态分离。上实例:
原文链接:https://juejin.cn/post/7332354240479707151 作者:森木阿