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

🎨 Bài 13: useInsertionEffect - CSS-in-JS & Inject Styles

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

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

1. useInsertionEffect Là Gì?

useInsertionEffect là hook chạy sớm nhất trong 3 effect hooks, TRƯỚC CẢ useLayoutEffect. Được thiết kế chỉ dành cho CSS-in-JS libraries (styled-components, Emotion, Linaria).

📌 Trình tự chính xác:
1️⃣ React render (Virtual DOM)
2️⃣ React commit (cập nhật DOM)
3️⃣ useInsertionEffect ← CHẠY ĐẦU TIÊN (inject styles)
4️⃣ useLayoutEffect ← CHẠY THỨ HAI (đo DOM)
5️⃣ Browser paint
6️⃣ useEffect ← CHẠY CUỐI CÙNG (side effects)
import { useEffect, useLayoutEffect, useInsertionEffect } from 'react';

function LifecycleDemo() {
    useInsertionEffect(() => {
        console.log('1️⃣ useInsertionEffect - inject CSS');
    });

    useLayoutEffect(() => {
        console.log('2️⃣ useLayoutEffect - đo DOM');
    });

    useEffect(() => {
        console.log('3️⃣ useEffect - side effects');
    });

    console.log('0️⃣ Render');
    return <div>Demo</div>;
}
// Output: 0️⃣ Render → 1️⃣ useInsertionEffect → 2️⃣ useLayoutEffect → 3️⃣ useEffect

2. Tại Sao Cần useInsertionEffect?

CSS-in-JS libraries cần inject <style> vào DOM trước khi bất kỳ layout effect nào chạy. Nếu dùng useLayoutEffect để inject styles, thì khi useLayoutEffect khác đo DOM, styles có thể chưa được áp dụng → đo sai!

// ❌ VẤN ĐỀ: dùng useLayoutEffect cho cả inject CSS và đo DOM
function Component() {
    useLayoutEffect(() => {
        // Inject CSS
        const style = document.createElement('style');
        style.textContent = '.box { width: 200px; padding: 20px; }';
        document.head.appendChild(style);
    }, []);

    useLayoutEffect(() => {
        // Đo DOM - nhưng CSS có thể CHƯA được áp dụng!
        const width = ref.current.offsetWidth; // Có thể sai!
    }, []);
}

// ✅ GIẢI PHÁP: useInsertionEffect cho CSS, luôn chạy trước
function Component() {
    useInsertionEffect(() => {
        // Inject CSS - CHẮC CHẮN chạy trước useLayoutEffect
        const style = document.createElement('style');
        style.textContent = '.box { width: 200px; padding: 20px; }';
        document.head.appendChild(style);
        return () => style.remove(); // Cleanup
    }, []);

    useLayoutEffect(() => {
        // Đo DOM - CSS đã được áp dụng, giá trị chính xác!
        const width = ref.current.offsetWidth; // ✅ Đúng!
    }, []);
}

3. Ví Dụ: Mini CSS-in-JS Engine

import { useInsertionEffect, useRef } from 'react';

// Mini CSS-in-JS helper
let styleCache = new Map();

function useCSS(rules) {
    useInsertionEffect(() => {
        const className = 'css-' + hashString(JSON.stringify(rules));

        if (!styleCache.has(className)) {
            const cssText = Object.entries(rules)
                .map(([prop, val]) => {
                    const cssProp = prop.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
                    return `${cssProp}: ${val}`;
                })
                .join('; ');

            const style = document.createElement('style');
            style.textContent = `.${className} { ${cssText} }`;
            document.head.appendChild(style);
            styleCache.set(className, style);
        }

        return () => {
            const style = styleCache.get(className);
            if (style) {
                style.remove();
                styleCache.delete(className);
            }
        };
    }, [JSON.stringify(rules)]);

    return 'css-' + hashString(JSON.stringify(rules));
}

function hashString(str) {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
        hash = ((hash << 5) - hash) + str.charCodeAt(i);
        hash |= 0;
    }
    return Math.abs(hash).toString(36);
}

// Sử dụng
function StyledButton({ primary }) {
    const className = useCSS({
        padding: '12px 24px',
        borderRadius: '8px',
        background: primary ? '#61DAFB' : '#ccc',
        color: primary ? 'white' : 'black',
        border: 'none',
        cursor: 'pointer',
        fontSize: '16px'
    });

    return <button className={className}>Click me</button>;
}

4. So Sánh 3 Effect Hooks

HookTimingUse CaseAi dùng?
useInsertionEffectTrước layout effectsInject <style> tagsLibrary authors
useLayoutEffectSau DOM, trước paintĐo DOM, ngăn flashApp developers
useEffectSau paintAPI, subscriptionsApp developers
⚠️ Quan trọng: useInsertionEffect được thiết kế chỉ cho CSS-in-JS library authors. Nếu bạn đang viết app thông thường, bạn gần như KHÔNG BAO GIỜ cần dùng hook này. React team khuyến cáo rõ ràng: "Đừng dùng nếu bạn không đang viết CSS-in-JS library."
💡 Hạn chế của useInsertionEffect:
• Không thể đọc refs (refs chưa attached)
• Không thể schedule state updates
• Không có access đến layout/paint information
• Chỉ nên dùng để insert/remove <style> nodes

📝 Tóm Tắt