You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中ref回调内使用ResizeObserver失效的原因解析

问题:为何React 18中在ref回调内调用ResizeObserver的observe会导致部分元素监听失效?

在React 18中实现基于ResizeObserver API的自定义Hook时,最初的实现如下:

const useResizeObserver = <T extends Element>(callback?: () => void) => {
  const [width, setWidth] = useState<number | undefined>();
  const [height, setHeight] = useState<number | undefined>();

  const observerRef = useRef<ResizeObserver>(new ResizeObserver((entries) => {
    const rect = entries[0].contentRect;
    setWidth(Math.round(rect.width));
    setHeight(Math.round(rect.height));
    callback?.();
  }));

  const ref: RefCallback<T> = useCallback(
    (element) => {
      if (element == null) return;
      observerRef.current.disconnect();
      observerRef.current.observe(element);
    },
    [],
  );

  return { ref, width, height };
}

该实现仅对部分元素触发回调,其余元素无响应。随后改用以下实现,功能恢复正常:

const useResizeObserver = <T extends Element>(callback?: () => void) => {
  const [width, setWidth] = useState<number | undefined>();
  const [height, setHeight] = useState<number | undefined>();

  const observerRef = useRef<ResizeObserver>(new ResizeObserver((entries) => {
    const rect = entries[0].contentRect;
    setWidth(Math.round(rect.width));
    setHeight(Math.round(rect.height));
    callback?.();
  }));

  const elRef = useRef<T|null>(null);

  useEffect(() => {
    const element = elRef.current;
    if (element == null) return;
    observerRef.current.observe(element);
    return () => observerRef.current.disconnect();
  }, []);

  const ref: RefCallback<T> = useCallback(
    (element) => {
      if (element == null) return;
      elRef.current = element;
    },
    [],
  );

  return { ref, width, height };
}

请问为何在ref回调内调用observe的实现会出现部分元素监听失效的问题?


原因分析

核心问题出在React 18 Strict Mode的双重渲染机制和ResizeObserver的disconnect行为的冲突上:

  • React 18 Strict Mode的重复执行:在开发环境的Strict Mode下,React会对组件初始化过程执行两次渲染,目的是帮开发者发现未清理的副作用。这会导致你的ref回调被执行两次:

    1. 第一次执行:拿到DOM元素后,调用disconnect()(此时Observer无监听目标,无影响),再调用observe(element)开始监听。
    2. 第二次执行:再次拿到同一个DOM元素,先调用disconnect()——这会直接终止刚刚建立的监听,随后再调用observe(element)重新注册。
      这个过程中,浏览器的ResizeObserver可能因为频繁的断开/注册,出现时序问题,导致最终监听未正确生效,从而丢失尺寸变化的回调。
  • 状态更新触发的ref回调重复执行:当组件因为width/height状态更新重新渲染时,即使ref是用useCallback包裹的空依赖,React仍可能在某些场景下再次执行ref回调。此时每次执行都会先断开之前的监听再重新注册,频繁的状态切换会导致监听不稳定,甚至丢失部分尺寸变化事件。

  • 修复方案的本质:修复后的代码把监听逻辑移到了useEffect中,依赖为空数组:

    • useEffect在组件挂载后仅执行一次(开发环境下Strict Mode会执行两次,但清理函数会先执行一次,再执行一次effect),但disconnect()作为清理函数只会在组件卸载时触发,避免了频繁断开监听。
    • 用elRef存储DOM元素,把ref回调的职责简化为仅存储元素,Observer的监听逻辑统一由useEffect管理,避免了ref回调执行时机带来的干扰。

另外,最初实现中无条件调用disconnect()的逻辑,虽然本身无错,但在重复执行场景下会频繁重置监听状态,进一步增加了不稳定的概率。


内容的提问来源于stack exchange,提问作者PSK

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 09:55:37