当 ref 指向 DOM 元素时,使用 ref.current 作为 useEffect 的依赖是不是安全?

Posted

技术标签:

【中文标题】当 ref 指向 DOM 元素时,使用 ref.current 作为 useEffect 的依赖是不是安全?【英文标题】:Is it safe to use ref.current as useEffect's dependency when ref points to a DOM element?当 ref 指向 DOM 元素时,使用 ref.current 作为 useEffect 的依赖是否安全? 【发布时间】:2021-06-14 04:33:23 【问题描述】:

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

当一个 ref 用于存储像 <div ref=ref> 这样的 DOM 元素时,并且当我开发一个依赖于该元素的自定义钩子时,假设 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]
  );
;

我读过 this comment 说 ref 应该在 useEffect 中使用,但我无法弄清楚 ref.current 更改但不会触发效果的任何情况。

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

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

虽然回调 ref 更难使用,因为强制用户编写它们:

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

<div ref=ref />

这就是为什么我要问在useEffect 中使用ref.current 是否安全。

【问题讨论】:

【参考方案1】:

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

React Hook useEffect 有一个不必要的依赖:'ref.current'。 排除它或删除依赖数组。可变值,例如 'ref.current' 不是有效的依赖项,因为改变它们不会 重新渲染组件。 (react-hooks/exhaustive-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>
    </>
  );
;


与此模式相关的现实生活用例是当我们希望有一个持久引用时,即使在元素卸载时

查看下一个示例,在该示例中,我们无法在卸载时保留元素大小。我们将尝试将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>
    </>
  );
;


令人惊讶的是,要修复它,我们需要直接处理 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>
    </>
  );
;

查看 React Docs 中的另一个示例:How can I measure a DOM node? 进一步阅读和更多示例参见uses of useEffect

【讨论】:

如果将 ref 传递给 DOM 元素,我认为每个 DOM 更改都是由渲染引起的,并且应该在提交渲染后触发效果,从比较 ref.current 开始,是否可能当它仅用于引用 DOM 时仍然会错过一些 ref 更新? 我没有一个例子说明 DOM 更改不会触发渲染,所以我想知道为什么在 useEffect 中使用 DOM ref.current 是不安全的 @otakustay 如果您在 DOM 上进行中继更改,为什么要使用 dep 数组?在没有 dep 数组的情况下将逻辑放在 useEffect 中。 ref.current 作为依赖项是无用的,如答案中所述 因为 ref 可以从 div 更改为 span,或者在多个渲染中从 null 更改为 div,但是空的 deps 数组只能在第一个挂载的 DOM 元素上运行一次效果。 @Slbox 这就像转发 ref 的正常模式,没关系。【参考方案2】:

2021 年答案:

这篇文章解释了使用 refs 和 useEffect:Ref objects inside useEffect Hooks:

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

同样如本文所述,官方的 react 文档现已更新为推荐的方法(即使用回调而不是 ref + 效果)。见How can I measure a DOM node?:

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。简单的事情就这么啰嗦了。【参考方案3】:

我遇到了同样的问题,我使用 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 

【讨论】:

【参考方案4】:

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

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;

【讨论】:

【参考方案5】:

我已经停止使用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  />

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

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

【讨论】:

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

以上是关于当 ref 指向 DOM 元素时,使用 ref.current 作为 useEffect 的依赖是不是安全?的主要内容,如果未能解决你的问题,请参考以下文章

Vue 通过ref获取DOM元素

Vue 通过ref获取DOM元素

vue.js中ref和$refs的使用及示例讲解

vue中的ref和$refs的使用

Vue从入门到实战6_学习

第二十一篇Vue中的ref和$refs