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回调被执行两次:
- 第一次执行:拿到DOM元素后,调用
disconnect()(此时Observer无监听目标,无影响),再调用observe(element)开始监听。 - 第二次执行:再次拿到同一个DOM元素,先调用
disconnect()——这会直接终止刚刚建立的监听,随后再调用observe(element)重新注册。
这个过程中,浏览器的ResizeObserver可能因为频繁的断开/注册,出现时序问题,导致最终监听未正确生效,从而丢失尺寸变化的回调。
- 第一次执行:拿到DOM元素后,调用
状态更新触发的ref回调重复执行:当组件因为
width/height状态更新重新渲染时,即使ref是用useCallback包裹的空依赖,React仍可能在某些场景下再次执行ref回调。此时每次执行都会先断开之前的监听再重新注册,频繁的状态切换会导致监听不稳定,甚至丢失部分尺寸变化事件。修复方案的本质:修复后的代码把监听逻辑移到了
useEffect中,依赖为空数组:useEffect在组件挂载后仅执行一次(开发环境下Strict Mode会执行两次,但清理函数会先执行一次,再执行一次effect),但disconnect()作为清理函数只会在组件卸载时触发,避免了频繁断开监听。- 用
elRef存储DOM元素,把ref回调的职责简化为仅存储元素,Observer的监听逻辑统一由useEffect管理,避免了ref回调执行时机带来的干扰。
另外,最初实现中无条件调用disconnect()的逻辑,虽然本身无错,但在重复执行场景下会频繁重置监听状态,进一步增加了不稳定的概率。
内容的提问来源于stack exchange,提问作者PSK
相关产品推荐
相关产品推荐

