React hooks之使用useRef动态创建dom ref对象
前几天遇到一个需求,需要使用useRef动态创建dom ref对象:
有一个数组(表示楼层数据),并且它是一个异步数据,在map的过程中需要为每个楼层动态添加ref对象,后续滚动曝光时需要做一些数据保存操作
之前对于hooks原理没有做太多研究,所以一上来就尝试这样写:
import { LegacyRef, useEffect, useRef, useState } from 'react';
export default function Home() {
const [data, setData] = useState<{ id: string; }[]>([]);
const refObj = useRef<{ [key: string]: LegacyRef<HTMLDivElement> }>({});
const changeData = () => {
setData([...data, { id: String(Math.random() * 100000) }]);
useEffect(() => {
setTimeout(() => {
setData([{ id: '2134' }])
}, 300);
}, []);
return (
<button onClick={changeData}>change data</button>
data.map((item) => {
refObj.current[item.id] = useRef<HTMLDivElement>();
return (
key={item.id}
ref={refObj.current[item.id]}
onClick={() => { console.log(refObj.current[item.id]); }}
> {item.id}</div >
很遗憾,直接报错,知道原理后,觉得实在是太愚蠢了,我们来打断点看一下源码
对于hook,react是这样缓存的,组件第一次初始化走的是mountXXX,组件后续更新走的是updateXXX,如下:
且hooks是从上到下顺序执行的,组件第一次初始化后,hooks调用的对应关系如下:
后续setData之后,会导致function重新执行,我们来看又发生了什么:
因为setData是更新操作,react内部会派发更新函数去处理数据状态,也就是updateRef,所以并没有走mountRef去创建新的ref对象,此时 更新的还是之前的refObj;
最根本的原因: 我们在map的回调函数中使用useRef,它和if一样,都会破坏hooks的调用顺序,从而出现以下错误:
总结一下,还是官网的那句话,使用hooks时,不要破坏它们的调用顺序,这就是为什么必须在我们组件的顶层调用 Hooks 的原因
TIPS:其实项目开启了hooks eslint的话,这样写vscode会直接就告警了,都不用运行
附:解决方案
值得注意的是,方案0对于异步数据无效(只有静态数据才有效),所以不推荐:
方案0 :
方案1(使用回调进行ref的动态赋值):
import { useEffect, useRef, useState } from 'react';
export default function Home() {
const [data, setData] = useState<{ id: string; }[]>([]);
const refObj = useRef<{ [key: string]: HTMLDivElement }>({});
const btnClick = () => {
setData([...data, { id: String(Math.random() * 100000) }]);
console.log(refObj.current);
useEffect(() => {
setTimeout(() => {
setData([{ id: '2134' }])
}, 300);
}, []);
return (
<button onClick={btnClick}>count+</button>
data.map((item) => (
key={item.id}
ref={r => { refObj.current[item.id] = r; }}
onClick={() => {
console.log(refObj.current[item.id].innerText);
> {item.id}</div >
方案2(使用use-dynamic-refs):
import { LegacyRef, RefObject, useEffect, useState } from 'react';
import useDynamicRefs from 'use-dynamic-refs';
export default function Home() {
const [count, setCount] = useState<{ id: string; }[]>([]);
const [getRef, setRef] = useDynamicRefs();
const btnClick = () => {
setCount([...count, { id: String(Math.random() * 100000) }]);
useEffect(() => {
setTimeout(() => {
setCount([{ id: '2134' }]);
}, 300);
}, []);
return (
<button onClick={btnClick}>count+</button>
{count.map((item) => (
key={item.id}
ref={setRef(item.id) as LegacyRef<HTMLDivElement>}
onClick={() => {
const target = (getRef(item.id) as RefObject<HTMLDivElement>).current as HTMLDivElement;
console.log(target.innerText);