← Về danh sách bài họcBài 12/25
📐 Bài 12: useLayoutEffect - Đo Lường & Thao Tác DOM
🎯 Sau bài học này, bạn sẽ:
- Hiểu sự khác biệt chính xác giữa useLayoutEffect và useEffect
- Biết khi nào BẮT BUỘC phải dùng useLayoutEffect
- Xây dựng Tooltip, Auto-resize Textarea, Animation
- Hiểu trình tự: render → DOM update → useLayoutEffect → paint → useEffect
- Tránh pitfalls và performance issues
1. useLayoutEffect vs useEffect - Sự Khác Biệt Cốt Lõi
Cả hai đều chạy sau khi DOM cập nhật, nhưng thời điểm khác nhau:
📌 Trình tự thực thi:
1️⃣ React render (tính Virtual DOM mới)
2️⃣ React commit (cập nhật DOM thật)
3️⃣ useLayoutEffect chạy (ĐỒNG BỘ - block paint)
4️⃣ Browser paint (vẽ pixel lên màn hình)
5️⃣ useEffect chạy (BẤT ĐỒNG BỘ - sau paint)
1️⃣ React render (tính Virtual DOM mới)
2️⃣ React commit (cập nhật DOM thật)
3️⃣ useLayoutEffect chạy (ĐỒNG BỘ - block paint)
4️⃣ Browser paint (vẽ pixel lên màn hình)
5️⃣ useEffect chạy (BẤT ĐỒNG BỘ - sau paint)
import { useState, useEffect, useLayoutEffect } from 'react';
function TimingDemo() {
const [value, setValue] = useState(0);
useEffect(() => {
console.log('3️⃣ useEffect - SAU khi browser paint');
});
useLayoutEffect(() => {
console.log('2️⃣ useLayoutEffect - TRƯỚC khi browser paint');
});
console.log('1️⃣ Render');
return <p>{value}</p>;
}
// Output: 1️⃣ Render → 2️⃣ useLayoutEffect → 3️⃣ useEffect2. Use Case 1: Ngăn Flash (Nhấp Nháy UI)
Đây là use case phổ biến nhất: khi bạn cần đo DOM rồi setState trước khi user nhìn thấy.
// ❌ useEffect: User thấy flash (vị trí nhảy)
function TooltipBad({ text, targetRef }) {
const [pos, setPos] = useState({ top: 0, left: 0 });
useEffect(() => {
const rect = targetRef.current.getBoundingClientRect();
setPos({ top: rect.top - 40, left: rect.left });
// Browser ĐÃ PAINT rồi → user thấy tooltip ở (0,0) rồi nhảy
}, []);
return <div style={{ position: 'fixed', ...pos }}>{text}</div>;
}
// ✅ useLayoutEffect: Không flash!
function TooltipGood({ text, targetRef }) {
const [pos, setPos] = useState({ top: 0, left: 0 });
useLayoutEffect(() => {
const rect = targetRef.current.getBoundingClientRect();
setPos({ top: rect.top - 40, left: rect.left });
// Browser CHƯA PAINT → setState → paint cùng lúc → không flash!
}, []);
return <div style={{ position: 'fixed', ...pos }}>{text}</div>;
}3. Use Case 2: Đo Kích Thước DOM
function AutoResizeTextarea() {
const textareaRef = useRef(null);
useLayoutEffect(() => {
const el = textareaRef.current;
// Reset height để đo scrollHeight chính xác
el.style.height = 'auto';
el.style.height = el.scrollHeight + 'px';
}); // Chạy mỗi render → textarea tự co giãn
return (
<textarea
ref={textareaRef}
onChange={() => {/* trigger re-render */}}
style={{ overflow: 'hidden', resize: 'none' }}
/>
);
}
// Đo kích thước element
function MeasureElement({ children }) {
const ref = useRef(null);
const [size, setSize] = useState({ width: 0, height: 0 });
useLayoutEffect(() => {
const { width, height } = ref.current.getBoundingClientRect();
setSize({ width: Math.round(width), height: Math.round(height) });
}, [children]);
return (
<>
<div ref={ref}>{children}</div>
<p>Kích thước: {size.width} x {size.height}</p>
</>
);
}4. Use Case 3: Animation Mượt
function SlideIn({ show }) {
const ref = useRef(null);
useLayoutEffect(() => {
if (show && ref.current) {
// Đặt vị trí ban đầu TRƯỚC khi paint
ref.current.style.transform = 'translateX(-100%)';
ref.current.style.transition = 'none';
// Force reflow
ref.current.getBoundingClientRect();
// Bắt đầu animation
ref.current.style.transition = 'transform 0.3s ease-out';
ref.current.style.transform = 'translateX(0)';
}
}, [show]);
if (!show) return null;
return <div ref={ref}>Slide in content!</div>;
}
// Scroll to position
function ScrollToTop() {
useLayoutEffect(() => {
// Scroll trước khi user thấy → không bị giật
window.scrollTo(0, 0);
});
return null;
}5. So Sánh Chi Tiết
| Tiêu chí | useEffect | useLayoutEffect |
|---|---|---|
| Timing | Sau paint (async) | Trước paint (sync) |
| Block paint | Không | Có - cẩn thận! |
| Dùng cho | API calls, subscriptions | Đo DOM, ngăn flash |
| Performance | Tốt hơn (non-blocking) | Có thể chậm nếu logic nặng |
| SSR | Hoạt động | Warning trên server |
⚠️ Quy tắc vàng: Luôn dùng
useEffect trước. Chỉ chuyển sang useLayoutEffect khi bạn thấy flash/nhấp nháy hoặc cần đo DOM trước paint. useLayoutEffect block browser paint, nên logic nặng trong đó sẽ khiến UI bị đơ.📝 Tóm Tắt
useLayoutEffectchạy đồng bộ sau DOM update, trước paint- Use cases: đo DOM, ngăn flash/nhấp nháy, animation setup
- useEffect: 95% cases | useLayoutEffect: 5% cases (khi cần đo DOM)
- ⚠️ Block paint → chỉ dùng khi thực sự cần, tránh logic nặng