← Về danh sách bài họcBài 12/25

📐 Bài 12: useLayoutEffect - Đo Lường & Thao Tác DOM

⏱️ Thời gian đọc: 20 phút | 📚 Độ khó: Nâng cao

🎯 Sau bài học này, bạn sẽ:

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)
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️⃣ useEffect

2. 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íuseEffectuseLayoutEffect
TimingSau paint (async)Trước paint (sync)
Block paintKhôngCó - cẩn thận!
Dùng choAPI calls, subscriptionsĐo DOM, ngăn flash
PerformanceTốt hơn (non-blocking)Có thể chậm nếu logic nặng
SSRHoạt độngWarning 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