一些 React 使用中常见的 hook

useState

最常见的 state hook 没什么好说的,每个学 react 的人第一个钩子

主要说明一下

React 18+ 批处理:将多个状态更新 (setState 调用) 合并为一个单一的更新批次,一起渲染。这样可以减少重新渲染的次数,从而提升性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState("");

const update = () => {
setCount(count + 1);
setName("React");
};

return (
<div>
<p>{count}</p>
<p>{name}</p>
<button onClick={update}>Update</button>
</div>
);
}

在旧版 React 中, setCountsetName 会在两次渲染中分别出发,即每次更新都会造成一次组件渲染

而在 React 18+, setCountsetName 会在被收集到一次渲染队列中,一起提交给后续渲染

1
2
3
4
5
const [cnt, setCnt] = setState(0);
function f() {
setCnt(cnt + 1);
setCnt(cnt + 1);
}

上述代码 cnt 变量并没有+2,而是+1,因为被批处理了,设置 Cnt 状态基于当前的 cnt,而第一次设置完后并没有立即渲染,所以第二次渲染的时候 cnt 还是 1+1 = 2

如果有这种依赖关系的渲染,请你使用回调函数,保证每次 setCnt 都会触发一次渲染

useRef

useRef 允许你创建一个持久化的引用,用于访问组件中的某个值或者 DOM 元素,并且不会触发重新渲染。可以说,useRef 是一个用于在多个渲染周期之间”存储数据”的容器。

工作原理

useRef 返回一个包含 current 属性的对象。这个对象在组件的整个生命周期都是持久化的,无论组件渲染多少次,它都指向一个相同的对象。

使用

  1. 请看如下代码
1
2
3
4
5
6
7
8
9
10
11
12
import React, { useRef, useEffect } from "react";

function FocusInput() {
const inputRef = useRef(null);

useEffect(() => {
// 使用 ref 获取 DOM 元素并操作
inputRef.current.focus();
}, []);

return <input ref={inputRef} />;
}

首先第一次渲染函数组件,返回一个绑定了 ref 的 input 元素。

之后再次渲染,对于 html 元素, react 采取的策略是如果元素的 state 和 props 没有发生改变,那么就不会创建一个新的元素,而是复用之前的元素

详细说说,第一次返回了一个带有 ref 的标签, ref 指向了 input 元素,第二次 useRef 返回的还是指向之前 input 元素的引用,而第二次 input 标签的 state,props 没有变化,所以使用的还是之前的 input 完成了 DOM 元素的记录

  1. 请看如下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React, { useState } from "react";

function App() {
const [count, setCount] = useState(1);

const increment = () => {
setCount(count + 1); // 每次点击按钮增加 count
};

return (
<div>
<button onClick={increment}>Increment count</button>
<Timer count={count} /> {/* 将 count 传递给 Timer 组件 */}
</div>
);
}

function Timer({ count }) {
const prevCountRef = useRef();

useEffect(() => {
prevCountRef.current = count; // 保存上次的值
}, [count]);

return (
<div>
当前 count: {count}, 上次 count: {prevCountRef.current}
</div>
);
}

每次点击,触发状态更新,传递一个新的状态数值给Timer组件,会重新调用 Timer 组件函数,里面的 useRef,useState 等钩子函数行为都是,查看当前 fiber tree 上是否有该 state,该 ref,如果有返回当前的 state,ref,如果没有,则创建对象并返回

所以上述 timer 运行原理,第一传入一个 count, 触发渲染,触发 useEffect,设置 current 但是设置完 ref.current 并不会触发重新渲染,故当前 ref.current 还是之前的值,undefined,就完成了记录,当前值和之前值

如果再次触发 count 改变的渲染,那么重复上面的过程,返回的对象是相同的,修改 ref.current 的数值,并不会立即触发渲染,所以又保存了之前的状态

  1. 当然还有一个应用,背后记录一些数据,不展示在页面上,不要重新触发渲染

保持不变的 react key

在动态渲染列表的时候,例如map(e=><li></li>) 如果不加 key, 那么 react 会根据元素的位置来判断元素是否和之前的元素保持一样,这样的做法会产生一些问题

例如,当删除列表中间的一个元素,那么后面的元素的位置都发生了变化,后面元素的 state 都将发生改变,不再保留之前的状态,这样会导致一些意想不到的错误。 但是如果加上key属性值,那么,key 值不变,react 就不会重新渲染这个元素,从而保持元素状态稳定性

useEffect / useLayoutEffect

要想说明白 useEffect, useLayoutEffect 必须先要理解 React 的渲染过程

jsx -> React.createReactelement -> virtual dom -> fiber tree -> commit -> dom -> useLayoutEffect return function -> useLayoutEffect -> 浏览器的绘制 -> useEffect return function -> useEffect

其中 useLayoutEffect 表示 useLayoutEffect 在 fiber tree 中注册的调用函数
其中 useLayoutEffect return function 表示 useLayoutEffect 返回的函数执行
其中 useEffect return function 表示 useEffect 返回的函数执行
其中 useEffect 表示 useEffect 在 fiber tree 中注册的调用函数

useLayoutEffect 注册函数发生在浏览器绘制之前,所以一定是一个同步函数,常用于测量一些浏览器 DOM 相关内容

而 useEffect 在浏览器绘制之后,常用来注册一些异步函数,比如请求数据等

useMemo / useCallback

两个钩子都用于性能优化,缓存计算结果和避免不必要的函数重新创建,从而避免不必要的重新渲染和计算。

useMemo: 缓存计算结果,只有在依赖项发生变化时才重新计算值

useCallback: 缓存函数,只有在依赖项发生变化时才重新创建函数

模式都是一样的

1
2
3
4
5
6
7
8
const memoizedValue = useMemo(() => {
// 复杂计算
return expensiveComputation();
}, [dependency1, dependency2]);

const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);

如果依赖项 dependency1,dependency2 不发生变化,那么就不会执行 memo 中注册的函数

如果依赖项 count 不发生变化,那么就不会返回 useCallback 中创建的函数

往往配合 useCallback 创建的函数放在 useMemo 的依赖项中

useContext

用于在函数组件中,订阅  React  上下文的变化,组件书中跨越多个层级传递数据,而不必通过层层的 props 传递

基于声明式标签,解决 props 钻问题