监听浏览器DOM变化:useEffect+useState与useSyncExternalStore孰优?
监听DOM变化:useEffect+useState vs useSyncExternalStore哪个更优?
问题背景
在监听浏览器DOM事件与变化时,传统的useEffect+useState方案和React 18引入的useSyncExternalStore方案都被官方文档提及适用场景:
useEffect文档描述
"Effect可让组件与外部系统保持同步……外部系统指不受React控制的代码,如使用window.addEventListener()和window.removeEventListener()的事件订阅。"
useSyncExternalStore文档描述
"添加useSyncExternalStore的另一个场景是订阅浏览器随时间变化的值……该值可能在React不知情的情况下改变,应使用useSyncExternalStore读取。"
ResizeObserver同时符合上述两种描述——它既是不受React控制的外部系统,也是可能在React不知情下变化的浏览器API,两种实现方案功能一致:
useEffect实现方案
function useElementSize() { const ref = useRef(null); const [size, setSize] = useState({ width: 0, height: 0 }); useEffect(() => { const element = ref.current; if (!element) return; const resizeObserver = new ResizeObserver(entries => { setSize({ width: entries[0].contentRect.width, height: entries[0].contentRect.height }); }); resizeObserver.observe(element); return () => resizeObserver.disconnect(); }, []); return [ref, size]; }
useSyncExternalStore实现方案
function useElementSize() { const ref = useRef(null); const cachedSnapshot = useRef({ width: 0, height: 0 }); const subscribe = useCallback((callback) => { const element = ref.current; if (!element) return () => {}; const resizeObserver = new ResizeObserver(callback); resizeObserver.observe(element); return () => resizeObserver.disconnect(); }, []); const getSnapshot = useCallback(() => { const element = ref.current; if (!element) return cachedSnapshot.current; const rect = element.getBoundingClientRect(); const newSize = { width: rect.width, height: rect.height }; // Return cached snapshot if values haven't changed if (cachedSnapshot.current.width === newSize.width && cachedSnapshot.current.height === newSize.height) { return cachedSnapshot.current; } cachedSnapshot.current = newSize; return newSize; }, []); const size = useSyncExternalStore(subscribe, getSnapshot); return [ref, size]; }
方案对比与推荐
React官方推荐
针对ResizeObserver这类场景,React官方更推荐使用useSyncExternalStore,它是专门为订阅外部可变数据源设计的API,能更好地适配React的并发渲染模式,解决传统方案在复杂场景下的一致性问题。
核心差异分析
并发渲染兼容性
useEffect+useState方案在并发模式下可能出现状态更新滞后或不一致:useEffect的执行时机在渲染完成后,外部数据源变化触发的setState更新可能与React的并发调度冲突,导致组件显示非最新状态。useSyncExternalStore天生适配并发模式,能确保组件读取的数据源快照始终一致,不会出现中间状态,在并发渲染时表现更稳定。
性能表现
useEffect方案每次调用setState都会触发组件重渲染,即使新状态与旧状态完全一致(需手动添加判断才能避免)。useSyncExternalStore通过getSnapshot函数可手动判断数据是否变化,仅当数据真正改变时才触发渲染,减少不必要的重渲染;其订阅机制更贴合React更新调度,额外开销更低。
边缘场景适配
- SSR与Suspense:
useSyncExternalStore支持初始化快照获取,能避免服务端渲染时的hydration不匹配问题;而useEffect在SSR中不会执行,可能导致客户端初始状态与服务端不一致。 - 多组件共享数据源:如果多个组件需要监听同一个元素的尺寸,
useSyncExternalStore可更高效地管理订阅,避免重复创建ResizeObserver实例;useEffect方案需手动封装共享逻辑才能实现类似效果。
- SSR与Suspense:
总结
如果你的项目使用React 18+的并发特性、涉及SSR/Suspense场景,或追求更稳定的状态同步与更优性能,优先选择useSyncExternalStore方案;若为简单小项目,无需考虑复杂场景,useEffect方案也能正常工作,但useSyncExternalStore更符合React未来发展方向。
内容的提问来源于stack exchange,提问作者Eliav Louski
相关产品推荐
相关产品推荐

