
React 学习笔记:useLayoutEffect Hook
发布于: 2026-06-08 14:08:19 更新于: 2026-06-08 14:08:29
useLayoutEffect 基础
什么是 useLayoutEffect?
useLayoutEffect 是 useEffect 的一个同步版本,它会在浏览器重新绘制屏幕之前执行。
useEffect:在浏览器完成绘制之后异步执行(不阻塞渲染)useLayoutEffect:在浏览器绘制之前同步执行(会阻塞渲染)
这意味着 useLayoutEffect 中的代码执行完毕后,浏览器才会进行下一次绘制。
语法结构
import { useLayoutEffect } from "react";
function MyComponent() {
useLayoutEffect(() => {
// 🔴 同步执行副作用(在浏览器绘制之前)
return () => {
// 🟡 清理副作用
};
}, [dependencies]); // 依赖数组(与 useEffect 相同)
return <div>...</div>;
}
与 useEffect 语法完全一致,唯一区别是执行时机。
执行时机详解
浏览器渲染流程
┌─────────────────────────────────────────────────────────┐
│ React 更新状态 │
│ ↓ │
│ React 调用 render(计算虚拟 DOM) │
│ ↓ │
│ React 提交变更到真实 DOM │
│ ↓ │
│ 🔴 useLayoutEffect 同步执行(阻塞绘制) │
│ ↓ │
│ 浏览器计算布局(Layout) │
│ ↓ │
│ 浏览器绘制屏幕(Paint) │
│ ↓ │
│ 🔵 useEffect 异步执行(不阻塞绘制) │
└─────────────────────────────────────────────────────────┘
代码验证
import { useEffect, useLayoutEffect, useState } from "react";
function TimingDemo() {
const [count, setCount] = useState(0);
// 🔴 先执行(同步,阻塞绘制)
useLayoutEffect(() => {
console.log("useLayoutEffect: 浏览器绘制前");
}, [count]);
// 🔵 后执行(异步,绘制后)
useEffect(() => {
console.log("useEffect: 浏览器绘制后");
}, [count]);
console.log("render: 组件渲染");
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>增加</button>
</div>
);
}
输出顺序:
render: 组件渲染
useLayoutEffect: 浏览器绘制前
useEffect: 浏览器绘制后
useEffect vs useLayoutEffect 对比
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | 浏览器绘制后 | 浏览器绘制前 |
| 是否阻塞绘制 | ❌ 不阻塞 | ✅ 阻塞 |
| 执行方式 | 异步(微任务/宏任务) | 同步(立即执行) |
| 适用场景 | 大多数副作用 | 需要同步读取/修改布局 |
| 对性能影响 | 较小 | 较大(阻塞绘制) |
| SSR 兼容性 | ✅ 兼容 | ❌ 不兼容(需条件判断) |
何时使用 useLayoutEffect?
使用 useLayoutEffect:
- 需要在浏览器绘制前同步读取或修改 DOM 布局
- 需要测量元素尺寸/位置并基于结果调整
- 需要避免视觉闪烁(如工具提示定位)
使用 useEffect(大多数情况):
- 发送网络请求
- 设置定时器
- 添加/移除事件监听
- 操作 DOM 但不需要同步读取布局
- 任何不需要阻塞绘制的副作用
常见使用场景
1. 测量 DOM 元素尺寸(避免闪烁)
当需要根据元素实际尺寸计算布局时,必须在绘制前完成:
import { useState, useRef, useLayoutEffect } from "react";
function Tooltip({ children, targetElement }) {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);
// ❌ 错误:useEffect 会导致闪烁
// useEffect(() => {
// const { height } = ref.current.getBoundingClientRect();
// setTooltipHeight(height);
// }, []);
// ✅ 正确:useLayoutEffect 在绘制前同步测量
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height);
}, []);
// 计算位置(避免超出视口)
let top = 0;
if (targetElement) {
const rect = targetElement.getBoundingClientRect();
top = rect.top - tooltipHeight - 8; // 8px 间距
// 如果上方空间不够,显示在下方
if (top < 0) {
top = rect.bottom + 8;
}
}
return (
<div
ref={ref}
style={{
position: "absolute",
top: `${top}px`,
left: "50%",
transform: "translateX(-50%)",
background: "#333",
color: "white",
padding: "8px 12px",
borderRadius: "4px",
zIndex: 1000,
}}
>
{children}
</div>
);
}
为什么 useEffect 不行?
使用 useEffect 的执行流程:
1. React 渲染 Tooltip(tooltipHeight = 0)
2. 浏览器绘制 Tooltip(位置错误,在顶部)
3. useEffect 执行,计算实际高度
4. React 重新渲染 Tooltip(位置正确)
5. 浏览器再次绘制(闪烁!)
使用 useLayoutEffect 的执行流程:
1. React 渲染 Tooltip(tooltipHeight = 0)
2. useLayoutEffect 同步执行,计算实际高度
3. React 重新渲染 Tooltip(位置正确)
4. 浏览器绘制 Tooltip(位置正确,无闪烁)
2. 动画起始状态
需要在动画开始前同步设置初始状态:
import { useState, useRef, useLayoutEffect } from "react";
function AnimatedBox({ isVisible }) {
const ref = useRef(null);
const [opacity, setOpacity] = useState(0);
useLayoutEffect(() => {
if (isVisible) {
// 先同步设置初始状态
setOpacity(0);
// 然后触发动画(下一帧)
requestAnimationFrame(() => {
setOpacity(1);
});
}
}, [isVisible]);
return (
<div
ref={ref}
style={{
width: 100,
height: 100,
background: "blue",
opacity,
transition: "opacity 0.3s",
}}
/>
);
}
3. 防止滚动闪烁
需要同步读取滚动位置并调整:
import { useLayoutEffect, useState } from "react";
function ScrollableContent({ children }) {
const [scrollY, setScrollY] = useState(0);
useLayoutEffect(() => {
// 同步读取滚动位置
const handleScroll = () => {
setScrollY(window.scrollY);
};
window.addEventListener("scroll", handleScroll, { passive: true });
// 初始读取
handleScroll();
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<div>
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
padding: "8px",
background: scrollY > 100 ? "rgba(0,0,0,0.8)" : "transparent",
color: scrollY > 100 ? "white" : "black",
transition: "background 0.2s",
}}
>
滚动位置: {scrollY}px
</div>
{children}
</div>
);
}
4. 同步更新 CSS 变量
根据 DOM 测量结果设置 CSS 变量:
import { useRef, useLayoutEffect, useState } from "react";
function DynamicLayout() {
const containerRef = useRef(null);
const [columns, setColumns] = useState(1);
useLayoutEffect(() => {
const container = containerRef.current;
if (!container) return;
// 同步测量容器宽度
const width = container.getBoundingClientRect().width;
// 根据宽度决定列数
const newColumns = width > 800 ? 3 : width > 400 ? 2 : 1;
if (newColumns !== columns) {
setColumns(newColumns);
}
}, []);
return (
<div
ref={containerRef}
style={{
display: "grid",
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: "16px",
padding: "16px",
}}
>
{Array.from({ length: 6 }).map((_, i) => (
<div
key={i}
style={{
background: "#f0f0f0",
padding: "16px",
borderRadius: "8px",
}}
>
内容 {i + 1}
</div>
))}
</div>
);
}
常见错误与陷阱
1. 滥用 useLayoutEffect
// ❌ 错误:网络请求不需要同步执行
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useLayoutEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
}, [userId]);
// ...
}
// ✅ 正确:网络请求使用 useEffect
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
}, [userId]);
// ...
}
原因: useLayoutEffect 会阻塞绘制,网络请求耗时不可控,会导致页面卡顿。
2. 在 useLayoutEffect 中执行耗时操作
// ❌ 错误:耗时操作会阻塞渲染
useLayoutEffect(() => {
// 假设这个操作需要 500ms
const result = performHeavyComputation();
setResult(result);
}, []);
// ✅ 正确:耗时操作使用 useEffect 或 Web Worker
useEffect(() => {
// 异步执行,不阻塞渲染
setTimeout(() => {
const result = performHeavyComputation();
setResult(result);
}, 0);
}, []);
3. 依赖数组问题
// ❌ 错误:依赖数组为空,但使用了外部变量
useLayoutEffect(() => {
console.log(count); // count 变化时不会重新执行
}, []);
// ✅ 正确:添加所有依赖
useLayoutEffect(() => {
console.log(count);
}, [count]);
补充:useInsertionEffect(React 18+)
什么是 useInsertionEffect?
useInsertionEffect 是 React 18 引入的新 Hook,专门用于CSS-in-JS 库在 DOM 变更后、布局效果之前同步插入样式。
执行时机
React 渲染 → 提交 DOM → useInsertionEffect(同步)→ useLayoutEffect(同步)→ 浏览器布局 → 浏览器绘制 → useEffect(异步)
语法结构
import { useInsertionEffect } from "react";
function MyComponent() {
useInsertionEffect(() => {
// 在这里插入样式(CSS-in-JS 场景)
return () => {
// 清理样式
};
}, [dependencies]);
return <div>...</div>;
}
主要用途
useInsertionEffect 主要用于 CSS-in-JS 库的开发者,普通应用开发者很少直接使用。
典型场景:CSS-in-JS 库
// 假设这是一个 CSS-in-JS 库的内部实现
function useCSS(styles) {
useInsertionEffect(() => {
// 1. 创建 <style> 标签
const style = document.createElement("style");
style.textContent = styles;
style.dataset.cssId = "dynamic-styles";
// 2. 插入到 <head>
document.head.appendChild(style);
// 3. 清理函数:移除样式
return () => {
document.head.removeChild(style);
};
}, [styles]);
}
// 使用示例
function Button() {
const styles = `
.btn {
background: blue;
color: white;
padding: 8px 16px;
}
`;
useCSS(styles);
return <button className="btn">点击我</button>;
}
为什么需要 useInsertionEffect?
问题:CSS-in-JS 的样式闪烁
使用 useEffect 的问题流程:
1. React 渲染组件
2. 浏览器绘制(无样式)
3. useEffect 执行,插入样式
4. 浏览器重新绘制(有样式)→ 闪烁!
使用 useInsertionEffect 的流程:
1. React 渲染组件
2. useInsertionEffect 同步插入样式
3. useLayoutEffect 执行(布局计算)
4. 浏览器绘制(有样式)→ 无闪烁!
对比三个 Hook
| Hook | 主要用途 | 适用人群 |
|---|---|---|
useInsertionEffect |
插入样式(CSS-in-JS) | CSS-in-JS 库开发者 |
useLayoutEffect |
读取/修改 DOM 布局 | 应用开发者 |
useEffect |
大多数副作用 | 应用开发者 |
实际应用:styled-components
styled-components v5.1+ 使用 useInsertionEffect 来优化样式注入:
// styled-components 内部简化实现
import { useInsertionEffect } from "react";
function styled(Component) {
return function StyledComponent(props) {
const styles = generateStyles(props);
useInsertionEffect(() => {
const style = document.createElement("style");
style.textContent = styles;
document.head.appendChild(style);
return () => {
document.head.removeChild(style);
};
}, [styles]);
return <Component {...props} />;
};
}
注意事项
- 普通开发者很少直接使用:除非你在开发 CSS-in-JS 库
- React 18+ 才支持:React 17 及以下版本不存在此 Hook
- SSR 兼容性:需要条件判断,与
useLayoutEffect类似 - 性能考虑:同步执行,会阻塞绘制
// SSR 兼容性处理
function useIsClient() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
return isClient;
}
function MyCSSInJS() {
const isClient = useIsClient();
useInsertionEffect(() => {
if (!isClient) return;
// 只在客户端执行
}, [isClient]);
}
总结
| 场景 | 推荐 Hook |
|---|---|
| 发送网络请求 | useEffect |
| 设置定时器 | useEffect |
| 添加/移除事件监听 | useEffect |
| 操作 DOM 但不需要同步读取布局 | useEffect |
| 测量 DOM 元素尺寸/位置 | useLayoutEffect |
| 动画起始状态 | useLayoutEffect |
| 防止视觉闪烁 | useLayoutEffect |
| 同步更新 CSS 变量 | useLayoutEffect |
| 插入样式(CSS-in-JS 库) | useInsertionEffect |
| 服务端渲染 | useEffect |
标签分类
# React# 前端