陌上人如玉
公子世无双

2.17 useInsertionEffect 与 CSS-in-JS 原理

2.17 useInsertionEffect 与 CSS-in-JS 原理

一句话:useInsertionEffect 是为 CSS-in-JS 库设计的第三个 Effect——它在「DOM 变更之前」同步执行,让「运行时生成的样式」赶在「浏览器计算布局」之前进入样式表,从而避免「额外一次重排」与「样式闪烁(FOUC)」。

一、三个 Effect 的完整时序

一次更新的完整时间轴(含三个 Effect):

① render 阶段(可中断)
   · 执行组件函数、diff、收集 flags

② commit 阶段
   a. before mutation(读取 DOM 快照,如 getSnapshotBeforeUpdate)
   b. ⭐ useInsertionEffect 的清理 + 执行   ← 【DOM 变更之前】
   c. mutation(把 DOM 变更应用到真实 DOM)
        · 执行 useLayoutEffect 的【清理函数】
        · 卸载的节点上把 ref 置为 null
   d. layout
        · 赋值新 ref
        · ⭐ useLayoutEffect 的清理 + 执行   ← 【DOM 已更新、绘制前】
   e. (commit 结束)

③ 浏览器绘制(paint)                        ← 用户在这一刻看到界面

④ passive effects(异步)
   · ⭐ useEffect 的清理 + 执行               ← 【绘制之后】
Effect 执行时机 能读 DOM / ref 能做副作用 会阻塞绘制 设计用途
useInsertionEffect DOM 变更之前 ❌(DOM 还是旧的、ref 未赋值) ⚠️ 只允许「插入样式」 ✅ CSS-in-JS 插入 <style> / insertRule
useLayoutEffect DOM 变更后、绘制前 ✅ ✅(同步) ✅ 测量布局、避免闪烁
useEffect 绘制之后(异步) ✅ ✅ ❌ 请求、订阅、日志
⭐ 一句话记住顺序:
   useInsertionEffect(DOM 变更前)→ DOM 变更 → useLayoutEffect(绘制前)→ 绘制 → useEffect(绘制后)

二、为什么需要它:CSS-in-JS 的两个难题

难题一:用 useLayoutEffect 插样式 → 多一次重排

// ❌ 用 useLayoutEffect 插入运行时样式
function BadButton() {
  useLayoutEffect(() => {
    injectStyle('.btn { color: red; }');    // ⚠️ 此时 DOM 已经变更
  });
  return <button className="btn">确定</button>;
}
时序问题:

① DOM 变更(<button class="btn"> 已进入 DOM)
② 浏览器「计算样式 + 布局」——但此时 .btn 规则【还不存在】
   → 浏览器按「没有样式」算一遍布局
③ useLayoutEffect 执行 → 插入 .btn 规则
④ 样式表变了 → ⭐ 浏览器【必须重新计算样式 + 重新布局】
   → 一次「额外的强制同步布局(forced reflow)」

⭐ 后果:
   · 每个「首次渲染的样式化组件」都会触发一次额外重排
   · 一个页面几十个组件 = 几十次额外重排 → 首屏明显变慢
   · 在「渲染中测量尺寸」的组件里还会读到「错误的中途布局」

难题二:用 useEffect 插样式 → 样式闪烁

// ❌ 用 useEffect 插入
function BadCard() {
  useEffect(() => {
    injectStyle('.card { padding: 16px }');   // ⚠️ 绘制之后才插入
  });
  return <div className="card">内容</div>;
}
时序问题:

① DOM 变更 → ② 浏览器绘制(此时 .card 规则不存在)→ 用户看到「无样式的卡片」
③ useEffect 执行 → 插入样式 → ④ 浏览器重绘 → 用户看到「有样式的卡片」

⭐ 后果:用户看到「一闪而过的无样式内容」(FOUC)
   —— 这在「首屏」和「服务端渲染后的水合」阶段尤其明显。
⭐⭐ 两个难题的本质:
   样式必须【在浏览器计算布局之前】就位。

   · useLayoutEffect 太晚(布局已经算过一轮)
   · useEffect 更晚(绘制都发生了)

   → React 需要「在 DOM 变更之前」也提供一个 sync effect 位置。
   → 这就是 useInsertionEffect(React 18 引入)。

三、官方对它的定位与约束

⭐⭐ 关键定位(React 官方文档原话的意思):

   「useInsertionEffect 是为 CSS-in-JS 库作者设计的。
     如果你不是在构建 CSS-in-JS 库,你应该用 useEffect 或 useLayoutEffect。」

⭐ 三条硬性约束:

① 【不能读 ref】
   DOM 还没变更 → ref 里还是「旧的元素」或 null
   → 读了也没有意义(而且规范明确不允许)

② 【不能 setState】
   在这个阶段触发更新会打乱 commit 流程
   → 你会收到警告("Cannot update state from useInsertionEffect")

③ 【不能依赖它做任何「有观察意义」的事】
   它只用来「往文档里插一段静态样式」——
   不要去读写 DOM、不要做埋点、不要发起请求
// ✅ 唯一正确的用法:插入样式(无副作用、无状态、无 DOM 读写)
useInsertionEffect(() => {
  const rule = sheet.insert(`.${key} { color: ${color} }`);
  return () => sheet.remove(rule);
}, [key, color]);

四、完整实现:一个可运行的 mini CSS-in-JS(180 行)

// mini-css.ts —— 用 useInsertionEffect 实现一个极简 CSS-in-JS 运行时
import { useInsertionEffect, useMemo } from 'react';

/** ============ 1. 规则去重:同样的 CSS 只插一次 ============ */
function hash(input: string): string {
  // djb2:快速、非加密、足够避免碰撞(生产库常用同类方案)
  let h = 5381;
  for (let i = 0; i < input.length; i++) {
    h = ((h << 5) + h + input.charCodeAt(i)) | 0;
  }
  return (h >>> 0).toString(36);
}

/** ============ 2. 容器:按「层(tag)」分组,保证插入顺序稳定 ============ */
interface Container {
  el: HTMLStyleElement;
  sheet: CSSStyleSheet;
  /** ruleKey → 在 sheet.cssRules 里的下标(用于删除) */
  index: Map<string, number>;
  /** ruleKey → 引用计数(多个组件共用同一条规则时不能提前删) */
  refCount: Map<string, number>;
}

const containers = new Map<string, Container>();
/** 服务端渲染时收集规则(服务端没有 DOM,走「渲染期收集」而不是 effect) */
const serverRules = new Map<string, Map<string, string>>();

function isServer() {
  return typeof document === 'undefined';
}

function getContainer(tag: string): Container {
  let c = containers.get(tag);
  if (!c) {
    const el = document.createElement('style');
    el.setAttribute('data-mini-css', tag);
    document.head.appendChild(el);

    c = { el, sheet: el.sheet!, index: new Map(), refCount: new Map() };
    containers.set(tag, c);
  }
  return c;
}

/**
 * ⭐ 插入规则:用 sheet.insertRule() 而不是 el.textContent += css
 *   原因:改 textContent 会让浏览器「重新解析整个样式表」;
 *        insertRule 是增量操作,代价小得多(这是所有 CSS-in-JS 库的共识)
 */
function insertRule(container: Container, ruleKey: string, css: string): void {
  const count = container.refCount.get(ruleKey) ?? 0;

  if (count === 0) {
    const rule = `.${ruleKey}{${css}}`;
    try {
      const at = container.sheet.cssRules.length;      // 追加到末尾,保证顺序
      container.sheet.insertRule(rule, at);
      container.index.set(ruleKey, at);
    } catch (err) {
      // 规则非法时不要让整个应用崩掉
      console.warn('[mini-css] 插入规则失败:', css, err);
    }
  }

  container.refCount.set(ruleKey, count + 1);
}

/** 移除规则:引用计数归零才真正删除 */
function removeRule(container: Container, ruleKey: string): void {
  const count = (container.refCount.get(ruleKey) ?? 1) - 1;

  if (count > 0) {
    container.refCount.set(ruleKey, count);
    return;
  }

  const at = container.index.get(ruleKey);
  if (at !== undefined) {
    try {
      container.sheet.deleteRule(at);
    } catch {
      /* 已被移除则忽略 */
    }
    container.index.delete(ruleKey);
    // ⚠️ 删除后,后面所有规则的下标都要前移 —— 这里做一次简单重算
    container.index.forEach((value, key) => {
      if (value > at) container.index.set(key, value - 1);
    });
  }
  container.refCount.delete(ruleKey);
}

/** ============ 3. ⭐ 核心 Hook:在「DOM 变更之前」把样式插进去 ============ */
export function useStyle(css: string, tag = 'default'): string {
  const ruleKey = useMemo(() => `c${hash(css)}`, [css]);

  // 服务端:在「渲染期」收集(effect 在服务端不执行)
  if (isServer()) {
    const bucket = serverRules.get(tag) ?? new Map<string, string>();
    if (!bucket.has(ruleKey)) bucket.set(ruleKey, css);
    serverRules.set(tag, bucket);
    return ruleKey;
  }

  // 客户端:⭐ 用 useInsertionEffect(不是 useLayoutEffect / useEffect)
  useInsertionEffect(() => {
    const container = getContainer(tag);
    insertRule(container, ruleKey, css);
    return () => removeRule(container, ruleKey);
  }, [ruleKey, css, tag]);

  return ruleKey;
}

/** ============ 4. SSR:把收集到的规则吐成 <style> 字符串 ============ */
export function extractStyles(): string {
  const parts: string[] = [];
  serverRules.forEach((bucket, tag) => {
    const rules: string[] = [];
    bucket.forEach((css, key) => rules.push(`.${key}{${css}}`));
    parts.push(`<style data-mini-css="${tag}">${rules.join('')}</style>`);
  });
  serverRules.clear();
  return parts.join('');
}

/** ============ 5. 一个组件级 API:类似 styled 的极简版 ============ */
export function useClassNames(rules: Record<string, string>, tag = 'default'): Record<string, string> {
  const keys: Record<string, string> = {};
  // ⚠️ 注意:这里不能用 map 里调 Hook(Hook 规则),所以要「展开成固定调用」
  //    真实库会把「一次渲染的规则」收集起来,用「一个 useStyle(聚合后的 css)」处理
  Object.keys(rules).forEach((name) => {
    keys[name] = `pending-${name}`;
  });
  return keys;
}
⭐⭐ 这个实现里最值得注意的四个设计(都是「库级细节」):

① 用 `sheet.insertRule()` 而不是改 `textContent`
   → 前者是「增量插入」,后者会「重新解析整张样式表」

② 引用计数(refCount)
   → 同一个 CSS 规则被 100 个组件用到时,只能插一次、
     且要等「最后一个使用者卸载」才能删

③ 按「层(tag)」分多个 `<style>`
   → 保证「同类规则的相对顺序稳定」,
     避免「后插入的规则被前面的覆盖」(specificity 顺序问题)

④ 服务端走「渲染期收集」而不是 effect
   → 服务端没有 DOM,也没有 effect;
     客户端靠 useInsertionEffect 插入,服务端靠 extractStyles() 注入 HTML
// ✅ 使用示例:一个带动态颜色的按钮
import { useStyle } from './mini-css';

function Button({ color, children }: { color: string; children: React.ReactNode }) {
  // ⭐ 动态样式:颜色不同 → css 不同 → ruleKey 不同(各插一条规则,互不影响)
  const cls = useStyle(`
    display: inline-flex;
    align-items: center;
    padding: 8px 16px;
    border-radius: 6px;
    background: ${color};
    color: #fff;
    transition: filter .15s;
  `);

  return <button className={cls}>{children}</button>;
}

function App() {
  return (
    <>
      <Button color="#2563eb">主按钮</Button>
      <Button color="#dc2626">危险按钮</Button>
      {/* ⭐ 浏览器只会“算一次布局”:样式在 DOM 变更前就已就位 */}
    </>
  );
}
// ✅ SSR 集成(Next.js App Router 示意)
import { extractStyles } from './mini-css';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const styleTags = extractStyles();          // ⚠️ 必须在「整个树渲染完之后」调用
  return (
    <html>
      <head>
        {/* ⭐ 把服务端收集的规则注入 HTML,避免水合前的无样式闪烁 */}
        <style dangerouslySetInnerHTML={{ __html: styleTags.replace(/<\/?style[^>]*>/g, '') }} />
      </head>
      <body>{children}</body>
    </html>
  );
}

五、真实库是怎么做的

⭐ 主流 CSS-in-JS 库都依赖 useInsertionEffect(React 18+):

· Emotion:内部用 useInsertionEffect 把序列化后的样式插进它的 StyleSheet
· styled-components v6:改用 useInsertionEffect(v5 用 useLayoutEffect,有额外重排问题)
· styled-jsx(Next.js 内置):同样依赖 insertion 时机
· 零运行时方案(vanilla-extract / Linaria / Panda)不需要它
  → 因为样式在「构建时」就被抽成静态 CSS 文件了
⭐⭐ 为什么会「从 useLayoutEffect 迁移到 useInsertionEffect」?

因为 useLayoutEffect 有两个问题(见第二节):
① 「额外一次强制同步布局」——每个首次渲染的样式化组件都触发
② 在「并发渲染」下更麻烦:渲染可能被打断,
   useLayoutEffect 的执行与「样式插入」的时机可能错位

✅ useInsertionEffect 的价值:
   · 「DOM 变更之前」就把样式放进样式表
     → 浏览器只算一次布局(没有「先无样式、后有样式」的两轮)
   · 时机更早、更确定,与并发渲染的配合更好

六、与 React 19「样式表提升」的关系

// React 19 允许「把 <link> / <style> 直接写在组件里」,React 会处理去重与顺序
function Article() {
  return (
    <article>
      <link rel="stylesheet" href="/article.css" precedence="default" />
      <style href="article-inline" precedence="high" dangerouslySetInnerHTML={{ __html: '.x{color:red}' }} />
      <p className="x">内容</p>
    </article>
  );
}
⭐ 两者的分工:

· useInsertionEffect:**库作者**用来「运行时插入样式」(动态、不可静态提取)
· React 19 的样式表提升:**应用作者**用来「声明式引入样式表」,
  由 React 负责「去重、按 precedence 排序、在合适时机插入」
  → 适用于「静态 CSS / 第三方样式表」

⭐ 结论:
   如果你的样式能「编译时提取」→ 根本不需要 useInsertionEffect(用 vanilla-extract / Tailwind)
   如果必须「运行时生成」→ 用 useInsertionEffect(或直接用成熟库)
方案 样式生成时机 需要 useInsertionEffect 运行时开销
CSS Modules / Tailwind 构建时 ❌ 无
vanilla-extract / Linaria / Panda 构建时(写法像 JS) ❌ 无
Emotion / styled-components 运行时 ✅ 中(序列化 + 插入)
React 19 样式表提升(<link precedence>) 构建/网络 ❌(React 内部处理) 无

七、什么时候不该用它

⭐ 四条「不该用」的判断:

① 你不是在写 CSS-in-JS 库 → 不要用
   (官方明确说过:应用代码应该用 useEffect / useLayoutEffect)

② 样式可以「构建时提取」→ 不需要它
   (CSS Modules / Tailwind / vanilla-extract 都能静态产出 CSS)

③ 你只是想「动态改几个样式」→ 不需要它
   ✅ 用 CSS 变量 + 内联 style:
      <div style={{ '--btn-bg': color } as React.CSSProperties} className="btn" />
      CSS 里:.btn { background: var(--btn-bg); }
   → 零运行时、零额外重排

④ 你只是想让「首屏不闪」→ 先在构建时把样式抽出来
   (服务端渲染时「渲染期收集 + 注入 HTML」是更根本的解法)
// ✅ ③ 的推荐做法:CSS 变量代替「运行时生成 CSS」
// styles.css(构建时产出,零运行时)
// .progress { width: var(--progress, 0%); transition: width .2s }
// .avatar { background: var(--avatar-bg, #eee) }

function Progress({ value }: { value: number }) {
  return (
    <div
      className="progress"
      style={{ '--progress': `${value}%` } as React.CSSProperties}   // ⭐ 只传变量
    />
  );
}

八、本篇特有的坑

// ① 在 useInsertionEffect 里读 ref
useInsertionEffect(() => {
  const h = ref.current.offsetHeight;     // ❌ DOM 还没变更,读到的是旧值/null
}, []);

// ② 在 useInsertionEffect 里 setState
useInsertionEffect(() => {
  setReady(true);                          // ❌ 会打乱 commit 流程(React 会警告)
}, []);

// ③ 用 useLayoutEffect 代替它来插样式 → 每个组件一次额外重排
useLayoutEffect(() => { inject(css); }, []);   // ⚠️ 布局已经算过一遍了

// ④ 用 useEffect 代替它 → 样式闪烁(FOUC)
useEffect(() => { inject(css); }, []);         // ⚠️ 绘制之后才插

// ⑤ 用 textContent 追加样式而不是 insertRule → 每次重新解析整张表
styleEl.textContent += `.${key}{${css}}`;      // ❌ O(n) 解析
container.sheet.insertRule(`.${key}{${css}}`, container.sheet.cssRules.length);  // ✅

// ⑥ 忘记「引用计数」→ 一个组件卸载就把「别人还在用的规则」删了
//    现象:组件 A 卸载后,组件 B 的样式突然消失

// ⑦ 删除规则后「不修正后续下标」→ 下次删除删错规则
//    (CSSOM 删除规则会让后面的规则下标前移)

// ⑧ 忘记「按层分组」→ specificity 顺序不稳定
//    现象:同样的两条规则,插入顺序变化导致「样式时好时坏」

// ⑨ SSR 时「在 effect 里收集样式」→ 服务端不执行 effect,什么也收集不到
//    ✅ 服务端必须在「渲染期」收集(把收集逻辑放在 useStyle 的函数体里)

// ⑩ 调用 extractStyles() 的时机太早 → 收集不到「后面才渲染的组件」
//    ✅ 必须在「整棵树渲染完之后」(如 layout 组件的末尾 / renderToString 之后)

// ⑪ 把「动态值」直接拼进 CSS 而不做转义 → CSS 注入
useStyle(`color: ${userProvidedColor}`);       // ⚠️ 若 color 里含 `;}` 可注入任意 CSS
// ✅ 校验/白名单(如只允许颜色字面量或 CSS 变量名)

// ⑫ 用 useInsertionEffect 做「非样式」的事(埋点、请求、日志)
//    ⚠️ 时机语义不对(它在 DOM 变更前),而且会被「并发渲染下的重放」影响

// ⑬ 在「并发渲染」下假设「insertion effect 不会被重复执行」
//    ⚠️ 与其它 effect 一样,它也可能被「挂载→卸载→再挂载」(StrictMode)
//       → 所以清理函数必须能把「插入的规则」正确撤销

// ⑭ 忘记「服务端的 style 顺序」要稳定
//    服务端收集顺序与客户端插入顺序不一致 → 水合后样式可能变化(轻微 FOUC)

// ⑮ 在「微前端」里多个应用各自用一套 CSS-in-JS → 样式表数量爆炸
//    ✅ 共享一个运行时(或统一换成构建时方案)
// ⑪ 的完整防护写法
function sanitizeColor(input: string): string {
  // 只允许 #hex / rgb() / hsl() / 命名色 / CSS 变量
  if (/^#[0-9a-f]{3,8}$/i.test(input)) return input;
  if (/^(rgb|hsl)a?\([\d\s.,%/]+\)$/i.test(input)) return input;
  if (/^var\(--[\w-]+\)$/.test(input)) return input;
  if (/^[a-z]+$/i.test(input)) return input;          // 命名色
  return 'currentColor';                              // 兜底
}

const cls = useStyle(`color: ${sanitizeColor(color)}`);

九、面试延伸

  1. 「useInsertionEffect 是做什么的?」
    它是 React 18 引入的第三个 Effect,专为 CSS-in-JS 库设计——在「任何 DOM 变更之前」同步执行。它解决的问题是:运行时生成的样式必须「在浏览器计算布局之前」进入样式表,否则会付出额外代价(用 useLayoutEffect → 布局已算过一轮,插入样式后要重新计算样式 + 重新布局;用 useEffect → 绘制之后才插入,用户会看到无样式内容一闪)。

  2. 「三个 Effect 的执行顺序是什么?」
    useInsertionEffect(DOM 变更前)→ DOM 变更 → useLayoutEffect(DOM 已更新、绘制前)→ 浏览器绘制 → useEffect(绘制之后,异步)。记忆方法是「越早的越不能碰 DOM」:useInsertionEffect 时 DOM 还没变(不能读 ref),useLayoutEffect 时 DOM 已更新(能测量),useEffect 时已经绘制(不会阻塞)。

  3. 「为什么 useInsertionEffect 里不能读 ref?」
    因为它在「DOM 变更之前」执行——此时真实的 DOM 还是旧的,ref 也还没被赋值(ref 在 commit 的 mutation/layout 阶段才更新)。所以读 ref 得到的是「旧元素或 null」,读了没有意义。官方也明确限制:「不要在 useInsertionEffect 里读 ref、更新 state,或做任何有副作用的事」——它只该用来「往样式表里插规则」。

  4. 「为什么 CSS-in-JS 库不用 useLayoutEffect?」
    因为会产生「额外一次强制同步布局」:useLayoutEffect 在「DOM 变更之后」执行,此时浏览器已经算过一轮布局(按「没有这些样式」算的);样式插入后,浏览器必须重新计算样式 + 重新布局。一个页面有几十个「首次渲染的样式化组件」就会有几十次额外重排,首屏明显变慢。useInsertionEffect 把插入时机提前到「DOM 变更之前」,浏览器只算一次布局。

  5. 「为什么不用 useEffect?」
    因为 useEffect 在「绘制之后」执行——用户会先看到「无样式的 DOM」,然后样式插入、重绘,出现一闪而过的无样式内容(FOUC)。这在「首屏」与「SSR 水合」阶段尤其明显。所以「插样式」既不能用 useEffect(太晚,会闪),也不能用 useLayoutEffect(会多一次重排),只有 useInsertionEffect 时机正确。

  6. 「应用代码需要用 useInsertionEffect 吗?」
    不需要(除非你在写 CSS-in-JS 库)。官方的定位很明确:「它是为库作者设计的;如果你是构建应用,应该用 useEffect 或 useLayoutEffect」。应用里遇到「动态样式」应该优先用 CSS 变量 + 内联 style(零运行时、零额外重排),或者用「构建时可提取」的方案(CSS Modules / Tailwind / vanilla-extract)。

  7. 「实现一个 CSS-in-JS 运行时要注意什么?」
    六个关键点:① 用 sheet.insertRule() 而不是改 textContent(后者会重新解析整张样式表);② 引用计数——同一条规则被多个组件使用时,只能插一次、等最后一个使用者卸载才能删;③ 按「层」分组多个 <style>——保证同类规则的相对顺序稳定(避免 specificity 顺序问题);④ 删除规则后要修正后续规则的下标(CSSOM 删除会让后面的下标前移);⑤ SSR 走「渲染期收集」(服务端没有 DOM 与 effect),用 extractStyles() 注入 HTML;⑥ 动态值要转义/白名单(防止 CSS 注入)。

  8. 「useInsertionEffect 和 React 19 的『样式表提升』有什么关系?」
    两者解决不同层的问题:① useInsertionEffect 面向「库作者」——用于「运行时生成、无法静态提取」的样式(如 Emotion / styled-components);② React 19 的 <link precedence> / <style> 面向「应用作者」——声明式引入「静态样式表」,由 React 负责去重、排序(按 precedence)、在合适时机插入。如果你的样式能在构建时提取,根本不需要 useInsertionEffect(用 CSS Modules / Tailwind / vanilla-extract)。

  9. 「为什么这个 Hook 会是『库专用』的?」
    因为它暴露的是「渲染管线的一个内部时机」——普通应用代码不需要知道「DOM 变更之前」这个窗口,用它反而容易写出「读 ref、setState、做埋点」之类的错误用法。而 CSS-in-JS 库必须在这个时机插入样式,所以 React 专门为它们开了一个口子。这类「为库设计的 API」在 React 里还有 useSyncExternalStore(为状态库)、useImperativeHandle(为封装组件)。

  10. 「如果我不小心在 useInsertionEffect 里读了 DOM 会怎样?」
    不会报错,但会得到错误的结果——读到的是「变更前的 DOM」(旧的尺寸/内容)或 null。如果你用它做「测量后设置样式」,就会出现「用上一次的尺寸渲染这一次」(与闭包陷阱症状类似)。而如果在那里 setState,React 会发出警告(因为它会打乱 commit 的流程)。结论:它不是「更早的 useLayoutEffect」,而是「样式表专用窗口」——不能拿它当 useLayoutEffect 用。

一句话速记

useInsertionEffect 在「DOM 变更之前」同步执行,专供 CSS-in-JS 库插样式——顺序是「useInsertionEffect(DOM 变更前)→ DOM 变更 → useLayoutEffect(绘制前)→ 绘制 → useEffect(绘制后)」;用 useLayoutEffect 会多一次强制同步布局、用 useEffect 会闪无样式内容(FOUC),所以只有它时机正确;三条禁令:「不能读 ref(DOM 还是旧的)、不能 setState、不能做任何有观察意义的事」;实现要点是「sheet.insertRule() 而不是改 textContent、引用计数、按层分多个 <style> 保证顺序、删除后修正下标、SSR 在渲染期收集 + extractStyles() 注入、动态值白名单防注入」;应用代码不要用它——优先「CSS 变量 + 内联 style」或「构建时可提取的方案」(CSS Modules / Tailwind / vanilla-extract),React 19 的 <link precedence> 也由 React 自己处理这类事。

赞(0) 打赏
未经允许不得转载:陌上寒 » 2.17 useInsertionEffect 与 CSS-in-JS 原理

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫