1. 基本概念
useInsertionEffect 是 React 18 引入的一个特殊的 Hook,它的执行时机比 useLayoutEffect 更早,主要用于在 DOM 变更之前注入样式。这个 Hook 主要面向 CSS-in-JS 库的开发者使用。
1.1 执行时机对比
jsx
function Component() {
useEffect(() => {
console.log('useEffect'); // 3. 最后执行
});
useLayoutEffect(() => {
console.log('useLayoutEffect'); // 2. 其次执行
});
useInsertionEffect(() => {
console.log('useInsertionEffect'); // 1. 最先执行
});
return <div>Example</div>;
}
2. 主要用途
2.1 动态注入样式
jsx
function useDynamicStyles(rule) {
useInsertionEffect(() => {
// 在 DOM 变更之前注入样式
const style = document.createElement('style');
style.textContent = rule;
document.head.appendChild(style);
return () => {
document.head.removeChild(style);
};
}, [rule]);
}
// 使用示例
function StyledComponent() {
const className = 'dynamic-style-' + Math.random().toString(36).slice(2);
useDynamicStyles(`
.${className} {
background-color: #f0f0f0;
padding: 16px;
border-radius: 4px;
}
`);
return <div className={className}>Styled content</div>;
}
2.2 CSS-in-JS 库实现
jsx
function createStyleSheet() {
let sheet = null;
let rules = new Map();
return {
insert(rule, key) {
if (!sheet) {
sheet = document.createElement('style');
document.head.appendChild(sheet);
}
if (!rules.has(key)) {
rules.set(key, rule);
sheet.textContent += rule;
}
},
remove(key) {
if (rules.has(key)) {
rules.delete(key);
if (sheet) {
sheet.textContent = Array.from(rules.values()).join('\\n');
}
}
}
};
}
const styleSheet = createStyleSheet();
function useStyles(styles) {
const key = useMemo(() => Math.random().toString(36).slice(2), []);
useInsertionEffect(() => {
// 在 DOM 变更之前注入样式
styleSheet.insert(styles, key);
return () => styleSheet.remove(key);
}, [styles, key]);
}
// 使用示例
function StyledButton({ primary }) {
const buttonStyles = \`
.button-\${primary ? 'primary' : 'secondary'} {
background: \${primary ? '#1a73e8' : '#ffffff'};
color: \${primary ? '#ffffff' : '#1a73e8'};
border: 1px solid #1a73e8;
padding: 8px 16px;
border-radius: 4px;
}
\`;
useStyles(buttonStyles);
return (
<button className={\`button-\${primary ? 'primary' : 'secondary'}\`}>
Click me
</button>
);
}
3. 性能优化
3.1 样式缓存
jsx
const styleCache = new Map();
function useOptimizedStyles(styles) {
const key = useMemo(() => JSON.stringify(styles), [styles]);
useInsertionEffect(() => {
if (!styleCache.has(key)) {
const styleElement = document.createElement('style');
styleElement.textContent = styles;
styleCache.set(key, styleElement);
document.head.appendChild(styleElement);
}
const element = styleCache.get(key);
let count = element.dataset.count || 0;
element.dataset.count = ++count;
return () => {
count = --element.dataset.count;
if (count === 0) {
document.head.removeChild(element);
styleCache.delete(key);
}
};
}, [key]);
}
3.2 批量样式注入
jsx
function useBatchStyles(styleRules) {
useInsertionEffect(() => {
const style = document.createElement('style');
const cssText = styleRules.join('\\n');
// 使用 requestAnimationFrame 批量注入样式
requestAnimationFrame(() => {
style.textContent = cssText;
document.head.appendChild(style);
});
return () => {
requestAnimationFrame(() => {
document.head.removeChild(style);
});
};
}, [styleRules]);
}
4. 实际应用场景
4.1 主题系统实现
jsx
function useThemeStyles(theme) {
const themeStyles = useMemo(() => {
return `
:root {
--primary-color: ${theme.primary};
--secondary-color: ${theme.secondary};
--text-color: ${theme.text};
--background-color: ${theme.background};
}
`;
}, [theme]);
useInsertionEffect(() => {
const style = document.createElement('style');
style.setAttribute('data-theme', 'true');
style.textContent = themeStyles;
// 移除旧主题
const oldTheme = document.querySelector('style[data-theme]');
if (oldTheme) {
document.head.removeChild(oldTheme);
}
document.head.appendChild(style);
return () => document.head.removeChild(style);
}, [themeStyles]);
}
4.2 动态组件样式
jsx
function useDynamicComponent(props) {
const { width, height, color } = props;
const styles = useMemo(() => `
.dynamic-component {
width: ${width}px;
height: ${height}px;
background-color: ${color};
transition: all 0.3s ease;
}
`, [width, height, color]);
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = styles;
document.head.appendChild(style);
return () => document.head.removeChild(style);
}, [styles]);
return <div className="dynamic-component" />;
}
5. 注意事项
-
使用限制
- 不能访问 refs
- 不能操作 DOM(除了注入样式)
- 主要用于 CSS-in-JS 库开发
-
执行时机
- 在 DOM 变更之前执行
- 比 useLayoutEffect 更早
- 在服务端渲染时不执行
-
性能考虑
- 避免频繁的样式变更
- 实现样式缓存机制
- 批量处理样式注入
-
最佳实践
- 仅用于样式注入
- 保持样式的可预测性
- 实现清理机制
6. 与其他 Hooks 的对比
Hook | 执行时机 | 主要用途 | 是否可以访问 DOM |
---|---|---|---|
useEffect | DOM 更新后异步执行 | 副作用处理 | 是 |
useLayoutEffect | DOM 更新后同步执行 | DOM 测量和更新 | 是 |
useInsertionEffect | DOM 更新前执行 | 样式注入 | 否(除样式注入外) |
通过合理使用 useInsertionEffect,我们可以优化 CSS-in-JS 的性能,避免样式注入导致的布局抖动。但要记住,这个 Hook 主要面向库的开发者,普通应用开发中很少直接使用。