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

监听浏览器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的并发渲染模式,解决传统方案在复杂场景下的一致性问题。

核心差异分析

  1. 并发渲染兼容性

    • useEffect+useState方案在并发模式下可能出现状态更新滞后或不一致:useEffect的执行时机在渲染完成后,外部数据源变化触发的setState更新可能与React的并发调度冲突,导致组件显示非最新状态。
    • useSyncExternalStore天生适配并发模式,能确保组件读取的数据源快照始终一致,不会出现中间状态,在并发渲染时表现更稳定。
  2. 性能表现

    • useEffect方案每次调用setState都会触发组件重渲染,即使新状态与旧状态完全一致(需手动添加判断才能避免)。
    • useSyncExternalStore通过getSnapshot函数可手动判断数据是否变化,仅当数据真正改变时才触发渲染,减少不必要的重渲染;其订阅机制更贴合React更新调度,额外开销更低。
  3. 边缘场景适配

    • SSR与Suspense:useSyncExternalStore支持初始化快照获取,能避免服务端渲染时的hydration不匹配问题;而useEffect在SSR中不会执行,可能导致客户端初始状态与服务端不一致。
    • 多组件共享数据源:如果多个组件需要监听同一个元素的尺寸,useSyncExternalStore可更高效地管理订阅,避免重复创建ResizeObserver实例;useEffect方案需手动封装共享逻辑才能实现类似效果。

总结

如果你的项目使用React 18+的并发特性、涉及SSR/Suspense场景,或追求更稳定的状态同步与更优性能,优先选择useSyncExternalStore方案;若为简单小项目,无需考虑复杂场景,useEffect方案也能正常工作,但useSyncExternalStore更符合React未来发展方向。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 14:55:07