useLayoutEffect 解决DOM操作页面闪烁问题的方法。
在React中,使用 useEffect 执行DOM操作时,它会在浏览器绘制之后执行,导致:
useEffect 中的DOM操作
浏览器重新绘制
这个"渲染 → 修改 → 再渲染"的过程可能导致用户看到页面闪烁。
useLayoutEffect 的解决方案useLayoutEffect 在浏览器绘制之前同步执行,所以DOM操作会和初始渲染一起完成。
import React, { useLayoutEffect, useRef, useState } from 'react';
function Example() {
const [width, setWidth] = useState(0);
const elementRef = useRef(null);
// 使用 useLayoutEffect 在绘制前测量并更新
useLayoutEffect(() => {
if (elementRef.current) {
const { width } = elementRef.current.getBoundingClientRect();
setWidth(width);
}
}, []);
return (
<div ref={elementRef}>
元素宽度: {width}px
</div>
);
}
function AutoFocusInput() {
const inputRef = useRef(null);
useLayoutEffect(() => {
// 在页面绘制前聚焦,用户不会看到未聚焦状态
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="自动聚焦" />;
}
function DynamicHeightBox() {
const [height, setHeight] = useState(0);
const contentRef = useRef(null);
useLayoutEffect(() => {
if (contentRef.current) {
const contentHeight = contentRef.current.scrollHeight;
// 立即设置高度,避免内容闪动
setHeight(contentHeight);
}
}, [content]); // content变化时重新测量
return (
<div style={{ height: height || 'auto' }}>
<div ref={contentRef}>
{/* 动态内容 */}
</div>
</div>
);
}
function ScrollSync({ list }) {
const listRef = useRef(null);
const [scrollTop, setScrollTop] = useState(0);
useLayoutEffect(() => {
const handleScroll = () => {
if (listRef.current) {
setScrollTop(listRef.current.scrollTop);
}
};
const element = listRef.current;
element?.addEventListener('scroll', handleScroll);
return () => element?.removeEventListener('scroll', handleScroll);
}, []);
return (
<div ref={listRef} style={{ overflow: 'auto', height: '400px' }}>
{/* 列表内容 */}
</div>
);
}
function FlashingComponent() {
const [visible, setVisible] = useState(false);
const divRef = useRef(null);
useEffect(() => {
// 这里会在绘制后执行,用户可能先看到div,然后立即隐藏
if (divRef.current) {
divRef.current.style.opacity = '0.5';
}
setVisible(true);
}, []);
return <div ref={divRef}>{visible ? '已加载' : '加载中...'}</div>;
}
function StableComponent() {
const [visible, setVisible] = useState(false);
const divRef = useRef(null);
useLayoutEffect(() => {
// 在绘制前设置,用户直接看到最终状态
if (divRef.current) {
divRef.current.style.opacity = '0.5';
}
setVisible(true);
}, []);
return <div ref={divRef}>{visible ? '已加载' : '加载中...'}</div>;
}
优先使用 useEffect
// 除非必要,否则优先使用 useEffect
// useLayoutEffect 会阻塞浏览器绘制
何时使用 useLayoutEffect
服务端渲染(SSR)注意事项
// 在SSR中,useLayoutEffect 会在服务端执行,可能产生警告
// 解决方案:条件使用
import { useEffect, useLayoutEffect } from 'react';
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
function MyComponent() {
useIsomorphicLayoutEffect(() => {
// DOM操作
}, []);
}
// 避免在 useLayoutEffect 中执行昂贵操作
useLayoutEffect(() => {
// ✅ 好的:DOM测量、同步样式更新
element.style.transform = `translateX(${x}px)`;
// ❌ 避免:大量计算、数据获取
// const data = expensiveCalculation();
// fetchData().then(...);
}, [x]);
| 方面 | useEffect |
useLayoutEffect |
|---|---|---|
| 执行时机 | 绘制之后,异步 | 绘制之前,同步 |
| 是否阻塞渲染 | 否 | 是 |
| 适用场景 | 数据获取、订阅、事件监听 | DOM测量、同步样式更新 |
| 页面闪烁 | 可能发生 | 避免闪烁 |
使用原则:只有在需要避免视觉不一致或闪烁时,才使用 useLayoutEffect。对于大多数副作用,useEffect 是更好的选择,因为它不会阻塞浏览器绘制。