假设你在开发一个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 的执行时机
- useEffect(异步):
• React 先完成 渲染,把新的 UI 绘制到屏幕上。
• 然后才执行 useEffect 里的代码。
• 如果 useEffect 里有状态更新,会触发第二次渲染(可能导致 UI 闪烁)。
- 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
| 特性 | useLayoutEffect | useEffect |
|---|---|---|
| 执行时机 | 渲染前(同步) | 渲染后(异步) |
| 是否阻塞 UI | ✅ 是 | ❌ 否 |
| 适用场景 | 获取 DOM 尺寸、滚动调整 | 数据请求、日志、订阅 |
| 避免 UI 闪烁 | ✅ 是 | ❌ 否 |
什么时候用 useLayoutEffect?
• 需要 测量 DOM 尺寸
• 需要 调整滚动
• 需要 确保 UI 不会闪烁
什么时候不该用 useLayoutEffect?
• 数据请求(fetch API)
• SSR 代码
• 复杂计算,影响性能