当 ref 指向 DOM 元素时,将 ref.current 用作 useEffect 的依赖项是否安全?

IT技术 reactjs
2021-04-10 06:35:38

我知道 ref 是一个可变容器,因此不应在useEffect的依赖项中列出它,但它ref.current可能是一个不断变化的值。

当 ref 用于存储像 DOM 元素时<div ref={ref}>,并且当我开发依赖于该元素的自定义钩子时,假设ref.current如果组件有条件地返回,则可以随着时间的推移而改变,例如:

const Foo = ({inline}) => {
  const ref = useRef(null);
  return inline ? <span ref={ref} /> : <div ref={ref} />;
};

我的自定义效果接收ref对象并ref.current用作依赖项是否安全

const useFoo = ref => {
  useEffect(
    () => {
      const element = ref.current;
      // Maybe observe the resize of element
    },
    [ref.current]
  );
};

我读过这条评论说 ref 应该用于useEffect,但我无法弄清楚任何情况下ref.current改变但效果不会触发。

正如那个问题所建议的那样,我应该使用回调引用,但是引用作为参数对于集成多个钩子非常友好:

const ref = useRef(null);
useFoo(ref);
useBar(ref);

虽然回调引用更难使用,因为强制用户组合它们:

const fooRef = useFoo();
const barRef = useBar();
const ref = element => {
  fooRef(element);
  barRef(element);
};

<div ref={ref} />

这就是为什么我要问ref.currentuseEffect.

5个回答

这是不是安全的,因为变异的引用不会触发渲染,因此,不会触发useEffect

React Hook useEffect 有一个不必要的依赖:'ref.current'。排除它或删除依赖项数组。像 'ref.current' 这样的可变值不是有效的依赖项,因为改变它们不会重新渲染组件。(react-hooks/详尽的deps)

反模式示例:

const Foo = () => {
  const [, render] = useReducer(p => !p, false);
  const ref = useRef(0);

  const onClickRender = () => {
    ref.current += 1;
    render();
  };

  const onClickNoRender = () => {
    ref.current += 1;
  };

  useEffect(() => {
    console.log('ref changed');
  }, [ref.current]);

  return (
    <>
      <button onClick={onClickRender}>Render</button>
      <button onClick={onClickNoRender}>No Render</button>
    </>
  );
};

编辑 xenodochial-snowflake-hhgr6


一个现实生活中的使用情况下,与此相关的模式是,当我们希望有一个持续性的参考,该元素卸除甚至当

检查下一个示例,其中我们无法在卸载时坚持调整元素大小。我们将尝试useRefuseEffect上述组合一起使用但它不起作用

// BAD EXAMPLE, SEE SOLUTION BELOW
const Component = () => {
  const ref = useRef();

  const [isMounted, toggle] = useReducer((p) => !p, true);
  const [elementRect, setElementRect] = useState();

  useEffect(() => {
    console.log(ref.current);
    setElementRect(ref.current?.getBoundingClientRect());
  }, [ref.current]);

  return (
    <>
      {isMounted && <div ref={ref}>Example</div>}
      <button onClick={toggle}>Toggle</button>
      <pre>{JSON.stringify(elementRect, null, 2)}</pre>
    </>
  );
};

编辑错误示例,Ref 不处理卸载


令人惊讶的是,为了修复它,我们需要node在记忆函数时直接处理useCallback

// GOOD EXAMPLE
const Component = () => {
  const [isMounted, toggle] = useReducer((p) => !p, true);
  const [elementRect, setElementRect] = useState();

  const handleRect = useCallback((node) => {
    setElementRect(node?.getBoundingClientRect());
  }, []);

  return (
    <>
      {isMounted && <div ref={handleRect}>Example</div>}
      <button onClick={toggle}>Toggle</button>
      <pre>{JSON.stringify(elementRect, null, 2)}</pre>
    </>
  );
};

编辑好例子,直接处理节点

因为 ref 可以从 div 更改为 span,或者在多个渲染中从 null 更改为 div,但空的 deps 数组只能在第一个挂载的 DOM 元素上运行一次 effect。
2021-06-04 06:35:38
@Slbox 这就像转发参考的正常模式,没关系。
2021-06-06 06:35:38
如果将 ref 传递给 DOM 元素,我认为每个 DOM 更改都是由渲染引起的,并且应在从 compare 开始提交渲染后触发效果ref.current,仅在使用时是否仍然会错过一些 ref 更新引用一个 DOM?
2021-06-12 06:35:38
@otakustay 如果您在 DOM 上进行中继,那么为什么要使用 dep 数组?将逻辑放在 useEffect 中,而无需使用 dep 数组。正如答案中提到的那样,ref.current 作为依赖项是无用的
2021-06-13 06:35:38
我无法举例说明 DOM 更改不会触发渲染,所以我想知道为什么在 useEffect 中使用 DOM ref.current 是不安全的
2021-06-15 06:35:38

2021 答案:

本文介绍的问题用参连同useEffect参考useEffect钩中的对象

如果将 useRef 钩子与跳过渲染的 useEffect 结合使用,则它可能成为自定义钩子的陷阱。您的第一直觉是将 ref.current 添加到 useEffect 的第二个参数,因此一旦 ref 更改它就会更新。但是 ref 直到您的组件渲染后才会更新——这意味着,任何跳过渲染的 useEffect 在下一次渲染之前都不会看到对 ref 的任何更改。

同样如本文所述,官方 react 文档现已更新为推荐方法(即使用回调而不是 ref + 效果)。请参阅如何测量 DOM 节点?

function MeasureExample() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  );
}
该死的,React 感觉就像在过去写 Java。对于简单的事情来说如此冗长。
2021-06-15 06:35:38

我遇到了同样的问题,我创建了一个带有 Typescript 的自定义钩子和一个带有 ref 回调的官方方法。希望它会有所帮助。

export const useRefHeightMeasure = <T extends HTMLElement>() => {
  const [height, setHeight] = useState(0)

  const refCallback = useCallback((node: T) => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height)
    }
  }, [])

  return { height, refCallback }
}

我遇到了类似的问题,其中我的 ESLint 抱怨ref.currentuseCallback. 我在我的项目中添加了一个自定义钩子来规避这个 eslint 警告。useCallback每当 ref 对象更改时,它都会切换变量以强制重新计算

import { RefObject, useCallback, useRef, useState } from "react";

/**
 * This hook can be used when using ref inside useCallbacks
 * 
 * Usage
 * ```ts
 * const [toggle, refCallback, myRef] = useRefWithCallback<HTMLSpanElement>();
 * const onClick = useCallback(() => {
    if (myRef.current) {
      myRef.current.scrollIntoView({ behavior: "smooth" });
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [toggle]);
  return (<span ref={refCallback} />);
  ```
 * @returns 
 */
function useRefWithCallback<T extends HTMLSpanElement | HTMLDivElement | HTMLParagraphElement>(): [
  boolean,
  (node: any) => void,
  RefObject<T>
] {
  const ref = useRef<T | null>(null);
  const [toggle, setToggle] = useState(false);
  const refCallback = useCallback(node => {
    ref.current = node;
    setToggle(val => !val);
  }, []);

  return [toggle, refCallback, ref];
}

export default useRefWithCallback;

我已经停止使用useRef,现在只使用useState一两次:

const [myChart, setMyChart] = useState(null)

const [el, setEl] = useState(null)
useEffect(() => {
    if (!el) {
        return
    }
    // attach to element
    const myChart = echarts.init(el)
    setMyChart(myChart)
    return () => {
        myChart.dispose()
        setMyChart(null)
    }
}, [el])

useEffect(() => {
    if (!myChart) {
        return
    }
    // do things with attached object
    myChart.setOption(... data ...)
}, [myChart, data])

return <div key='chart' ref={setEl} style={{ width: '100%', height: 1024 }} />

对于图表、身份验证和其他非react库很有用,因为它保留了一个元素 ref 和初始化的对象,并且可以根据需要直接处理它

我现在不确定为什么useRef首先存在......?

Ref 更改不会触发重新渲染。有可能你的一些组件触发了重新渲染,并且你的 ref 也发生了变化并被你的useEffect.
2021-05-22 06:35:38
但是我们确实希望在分配 DOM 元素后捕获这些情况。这就是为什么useRef在大多数情况下没有那么有用以及为什么我认为它不应该连接到元素的ref={ref}. 根据文档,推荐的方法似乎是使用“回调引用”(参见 Gyum Fox 的回答)。我在想 auseState也可以工作并且更简单。
2021-05-26 06:35:38
基于丹在这里的评论:github.com/facebook/react/issues/14387#issuecomment-503616820 Refs are for values whose changes don't need to trigger a re-render.我同意这个说法,有些情况下你不需要重新渲染,只需要保留一些东西的参考您的代码,例如组件、计数器等。
2021-05-26 06:35:38
奇怪,所以ref在一个return <div ref={ref}>不打算与useRef? 因为我确实想在div分配元素后触发块的执行,并且触发通常是用 完成的useEffect,就像在问题中一样。
2021-06-03 06:35:38