useLayoutEffect:优雅操作DOM,避免UI闪烁

187 阅读3分钟

假设你在开发一个React应用,其中有一个组件需要获取DOM元素的宽度并展示出来,比如下面这个简单的例子:

function MyComponent() {
  const [width, setWidth] = useState(0);

  useEffect(() => {
    const box = document.getElementById("box");
    if (box) {
      setWidth(box.clientWidth);
    }
  }, []);

  return (
    <div>
      <div id="box" style={{ width: "200px", height: "100px", background: "lightblue" }}>
        Box
      </div>
      <p>Box width: {width}px</p>
    </div>
  );
}

你可能会发现,第一次渲染时 width 先显示 0,然后才变成 200。如果你在界面上有复杂的布局或者动画,这种“闪烁”可能会变得非常明显,影响用户体验。

这个问题的根源在于 useEffect 是异步执行的,React 先渲染 UI,然后再执行 useEffect 里的代码。所以当 useEffect 读取 clientWidth 时,它拿到的是初始值,而不是最新的值

💡 那么如何确保获取 DOM 数据时,不会导致 UI 先渲染一次错误的值? 这时候 useLayoutEffect 就派上用场了!

什么是useLayoutEffect

useLayoutEffect 是 React 16.8 版本引入的 Hooks 之一,它的作用和 useEffect 类似,都是用来执行副作用(side effects)。但不同点在于 useLayoutEffect 是同步执行的,而 useEffect 是异步执行的

📌 useEffect 和 useLayoutEffect 的执行时机

  1. useEffect(异步):

• React 先完成 渲染,把新的 UI 绘制到屏幕上。

• 然后才执行 useEffect 里的代码。

• 如果 useEffect 里有状态更新,会触发第二次渲染(可能导致 UI 闪烁)。

  1. useLayoutEffect(同步):

• React 先完成 渲染,但在绘制到屏幕之前,立即执行 useLayoutEffect 里的代码。

• 这样我们可以在用户真正看到 UI 之前修改 DOM,避免 UI 先渲染一次错误的状态。

useEffect(() => {
  console.log("useEffect 运行");
});

useLayoutEffect(() => {
  console.log("useLayoutEffect 运行");
});

如果你在浏览器里运行上面的代码,你会发现 useLayoutEffect 总是先执行,然后才是 useEffect。

useLayoutEffect的使用场景

useLayoutEffect 适用于必须在渲染完成后立即同步执行副作用的场景,主要包括以下几种:

1:获取 DOM 元素的尺寸

如果你需要在 UI 渲染完成后立即测量一个 DOM 元素的尺寸,并更新 UI,useLayoutEffect 比 useEffect 更合适。

❌ 使用 useEffect,会导致 UI 先显示错误值:

function Example() {
  const [height, setHeight] = useState(0);
  const divRef = useRef(null);

  useEffect(() => {
    if (divRef.current) {
      setHeight(divRef.current.clientHeight);
    }
  }, []);

  return (
    <div>
      <div ref={divRef} style={{ height: "200px", background: "lightcoral" }} />
      <p>高度:{height}px</p>
    </div>
  );
}

使用 useLayoutEffect,避免 UI 闪烁:

useLayoutEffect(() => {
  if (divRef.current) {
    setHeight(divRef.current.clientHeight);
  }
}, []);

这样 height 在绘制到屏幕前就已经更新,不会出现 0px 的错误值。

2:滚动调整(滚动到特定位置)

如果你想在组件更新后自动滚动到某个位置,使用 useLayoutEffect 能确保滚动发生在 UI 渲染前,避免滚动跳跃。

useLayoutEffect(() => {
  window.scrollTo(0, 0);
}, []);

3:动画过渡

如果你需要手动操作 CSS 样式,例如 opacity 或 transform,确保动画不会出现“闪烁”或者“延迟” ,可以使用 useLayoutEffect。

useLayoutEffect(() => {
  document.getElementById("box").style.opacity = "1";
}, []);

useLayoutEffect的使用误区

虽然 useLayoutEffect 很强大,但如果滥用它,可能会影响性能甚至导致错误。

❌ 误区 1:滥用 useLayoutEffect 可能影响性能

useLayoutEffect 会阻塞渲染,如果你的副作用逻辑比较重(如复杂计算、网络请求),可能会导致页面卡顿。

正确做法:非 UI 相关的副作用(如 API 调用)应该用 useEffect

useEffect(() => {
  fetchData();
}, []);

❌ 误区 2:在服务器端渲染(SSR)中使用

React 在 SSR 时不会执行 useLayoutEffect,如果你在 Next.js 这样的环境里使用它,可能会导致警告或错误。

正确做法:在浏览器端判断再执行

if (typeof window !== "undefined") {
  useLayoutEffect(() => {
    console.log(window.innerWidth);
  }, []);
}

总结

useLayoutEffect vs useEffect

特性useLayoutEffectuseEffect
执行时机渲染前(同步)渲染后(异步)
是否阻塞 UI✅ 是❌ 否
适用场景获取 DOM 尺寸、滚动调整数据请求、日志、订阅
避免 UI 闪烁✅ 是❌ 否

什么时候用 useLayoutEffect?

• 需要 测量 DOM 尺寸

• 需要 调整滚动

• 需要 确保 UI 不会闪烁

什么时候不该用 useLayoutEffect?

数据请求(fetch API)

SSR 代码

复杂计算,影响性能