相关文章推荐
礼貌的土豆  ·  python中输出表格 ...·  3 年前    · 
胡子拉碴的大葱  ·  java ...·  3 年前    · 
React hooks之使用useRef动态创建dom ref对象

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,如下:
useState
useRef
且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):

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);