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

ota*_*tay 44 reactjs

我知道 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} />;
};
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

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

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

<div ref={ref} />
Run Code Online (Sandbox Code Playgroud)

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

Den*_*ash 36

因为变异的参考它没有安全不会引发渲染,因此,不会触发useEffect

React Hook useEffect 有一个不必要的依赖:'ref.current'。排除它或删除依赖项数组。像 'ref.current' 这样的可变值不是有效的依赖项,因为改变它们不会重新渲染组件。(反应钩子/详尽的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>
    </>
  );
};
Run Code Online (Sandbox Code Playgroud)

编辑 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>
    </>
  );
};
Run Code Online (Sandbox Code Playgroud)

编辑错误示例,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>
    </>
  );
};
Run Code Online (Sandbox Code Playgroud)

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

  • 因为 ref 可以在多个渲染中从 div 更改为 span,或者从 null 更改为 div,但空 deps 数组只能在第一个安装的 DOM 元素上运行一次效果。 (2认同)
  • @Slbox 这就像转发引用的正常模式,没关系。 (2认同)

Gyu*_*Fox 11

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>
    </>
  );
}
Run Code Online (Sandbox Code Playgroud)

  • 该死,React 感觉就像以前写 Java 一样。对于简单的事情来说这么多冗长的内容。 (67认同)
  • 回来 JQuery 一切都被原谅了! (9认同)
  • 然后你仍然需要ResizeObserver。来自官方文档:“在此示例中,仅当组件安装和卸载时才会调用回调引用,因为渲染的 &lt;h1&gt; 组件在任何重新渲染过程中都保持存在。如果您希望在组件调整大小时收到通知,您可能需要使用 ResizeObserver 或基于其构建的第三方 Hook。 (5认同)
  • 是的,一想到功能组件已经与类组件相比有了很大的差距,我就感到害怕 (2认同)

Kam*_*cki 5

我遇到了同样的问题,我使用 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 }
}
Run Code Online (Sandbox Code Playgroud)