← Về danh sách bài họcBài 13/25
🎨 Bài 13: useInsertionEffect - CSS-in-JS & Inject Styles
🎯 Sau bài học này, bạn sẽ:
- Hiểu useInsertionEffect và vị trí trong lifecycle
- Biết tại sao hook này tồn tại (CSS-in-JS performance)
- So sánh 3 effect hooks: useInsertionEffect vs useLayoutEffect vs useEffect
- Xem ví dụ thực tế từ styled-components và Emotion
- Hiểu khi nào KHÔNG nên dùng hook này
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)
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️⃣ useEffect2. 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
| Hook | Timing | Use Case | Ai dùng? |
|---|---|---|---|
useInsertionEffect | Trước layout effects | Inject <style> tags | Library authors |
useLayoutEffect | Sau DOM, trước paint | Đo DOM, ngăn flash | App developers |
useEffect | Sau paint | API, subscriptions | App 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
• 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
useInsertionEffect: chạy sớm nhất, dành cho inject CSS- Thứ tự:
useInsertionEffect→useLayoutEffect→ paint →useEffect - Giải quyết vấn đề CSS-in-JS: styles phải inject trước khi đo DOM
- Chỉ dành cho library authors, app developers dùng useEffect/useLayoutEffect