React 中hooks之useInsertionEffect用法总结

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. 注意事项

  1. 使用限制

    • 不能访问 refs
    • 不能操作 DOM(除了注入样式)
    • 主要用于 CSS-in-JS 库开发
  2. 执行时机

    • 在 DOM 变更之前执行
    • 比 useLayoutEffect 更早
    • 在服务端渲染时不执行
  3. 性能考虑

    • 避免频繁的样式变更
    • 实现样式缓存机制
    • 批量处理样式注入
  4. 最佳实践

    • 仅用于样式注入
    • 保持样式的可预测性
    • 实现清理机制

6. 与其他 Hooks 的对比

Hook 执行时机 主要用途 是否可以访问 DOM
useEffect DOM 更新后异步执行 副作用处理
useLayoutEffect DOM 更新后同步执行 DOM 测量和更新
useInsertionEffect DOM 更新前执行 样式注入 否(除样式注入外)

通过合理使用 useInsertionEffect,我们可以优化 CSS-in-JS 的性能,避免样式注入导致的布局抖动。但要记住,这个 Hook 主要面向库的开发者,普通应用开发中很少直接使用。

相关推荐
尾善爱看海5 小时前
Vue 面试收官篇:SSR、性能优化落地、30 道高频面试题精讲(附标准答案)
前端·javascript·vue.js·面试·vue
驳是6 小时前
一个组件,提升 react-router 开发的幸福感
前端·preact
GreenTea6 小时前
OpenAI Agents API 上手实测:一次调用把整个 agent loop 甩给 OpenAI
前端·后端·算法
星云API技术支持6 小时前
企业微信二次开发:群权限设置、成员管理与群资料维护的接口组合实践
java·前端·企业微信
京东云开发者7 小时前
81.8 秒的视频,我们改了 15 个版本:一次纯 Codex 驱动的 AI-native 视频实践
前端·aigc
Csvn7 小时前
TypeScript 大型项目架构:从单体 tsconfig 到分层可扩展的工程
前端
计算机魔术师9 小时前
Dario Amodei 发文呼吁为前沿 AI 降速并提出三点计划
前端
计算机魔术师9 小时前
OpenAI的AI代理偷偷给RubyGems下毒,我们却毫无察觉
前端
甲维斯9 小时前
0代码,0建模,3句话开发一个3D游戏!
前端·游戏·游戏开发
IT_陈寒9 小时前
Vue的响应式更新把我坑惨了,原来问题出在这
前端·人工智能·后端