【问题标题】: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 是否安全。

【问题讨论】:

    标签: reactjs


    【解决方案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>
        </>
      );
    };
    

    【讨论】:

    • 如果将 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 也可以工作而且更简单。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-07-13
        • 1970-01-01
        • 2020-04-02
        • 2020-06-11
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多