0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Qiita Advent Calender 2025企画】React Hooks を Hackしよう!【Part9: useInsertionEffectをふかぼってみよう】

Posted at

useInsertionEffect は React 18 で追加された特殊なエフェクトフックで、CSS-in-JS ライブラリの作者向けに設計されています。useLayoutEffect よりもさらに早く実行され、レイアウト読み取りが行われる前にスタイルを DOM に挿入するために使用します。

⚠️ 一般的なアプリケーション開発者への注意
useInsertionEffect は CSS-in-JS ライブラリの内部実装向けです。通常のアプリケーション開発では useEffect または useLayoutEffect を使用してください。スタイルを動的に挿入する特別な理由がない限り、このフックは必要ありません。

1. なぜ useInsertionEffect が必要か

1.1 CSS-in-JS の課題

CSS-in-JS ライブラリ(styled-components, Emotion など)は、コンポーネントのレンダー時に動的にスタイルを生成し、<style> タグとして DOM に挿入します。

しかし、スタイル挿入のタイミングが非常に重要です。

// ❌ 問題のあるパターン: useLayoutEffect でスタイルを挿入
function Component() {
  useLayoutEffect(() => {
    // スタイルを挿入
    const style = document.createElement('style');
    style.textContent = '.box { width: 100px; }';
    document.head.appendChild(style);
  }, []);

  useLayoutEffect(() => {
    // 別の useLayoutEffect で DOM を測定
    // → スタイルがまだ適用されていない可能性がある!
    const width = ref.current.getBoundingClientRect().width;
    console.log(width); // 期待した 100px ではないかも...
  }, []);

  return <div ref={ref} className="box" />;
}

問題: useLayoutEffect でスタイルを挿入すると、別のコンポーネントの useLayoutEffect でレイアウト測定を行う際に、スタイルがまだ適用されていない可能性があります。

1.2 useInsertionEffect が解決すること

useInsertionEffectすべての useLayoutEffect より前に実行されることが保証されています。

// ✅ 正しいパターン: useInsertionEffect でスタイルを挿入
function Component() {
  useInsertionEffect(() => {
    // 最初に実行される!
    const style = document.createElement('style');
    style.textContent = '.box { width: 100px; }';
    document.head.appendChild(style);
  }, []);

  useLayoutEffect(() => {
    // スタイルが適用された後に実行される
    const width = ref.current.getBoundingClientRect().width;
    console.log(width); // 100px ✓
  }, []);

  return <div ref={ref} className="box" />;
}

1.3 3つのエフェクトフックの実行順序

実行順序 フック 実行タイミング 主な用途
1️⃣ useInsertionEffect DOM 変更後、レイアウト読み取り CSS-in-JS のスタイル挿入
2️⃣ useLayoutEffect DOM 変更後、ブラウザ描画 DOM 測定、レイアウト計算
3️⃣ useEffect ブラウザ描画(非同期) データ取得、購読、ログ記録

1.4 useInsertionEffect の API

useInsertionEffect(setup, dependencies?)
引数 説明
setup 副作用のロジックを含む関数。クリーンアップ関数を返すことができる
dependencies 省略可能。依存配列。この値が変わった時にエフェクトが再実行される

返り値: undefined

1.5 重要な制約

useInsertionEffect には他のエフェクトフックにはない特別な制約があります:

制約 理由
state を更新できない 実行タイミングが早すぎるため、state 更新は予期しない動作を引き起こす
ref にアクセスできない 実行時点ではまだ ref がアタッチされていない
DOM の更新状態が不確定 DOM 更新の前後どちらで実行されるか保証されない
function Component() {
  const ref = useRef(null);
  const [count, setCount] = useState(0);

  useInsertionEffect(() => {
    // ❌ これらはダメ!
    setCount(1);              // state 更新は不可
    console.log(ref.current); // null の可能性がある
  }, []);

  return <div ref={ref}>{count}</div>;
}

2. useInsertionEffect の内部構造を徹底解剖

2.0 全体像: useInsertionEffect の処理フロー

🎣 useInsertionEffect(フック呼び出し)
   ↓
📝 Effect オブジェクトを作成(HookInsertion フラグ付き)
   ↓
📋 updateQueue に追加
   ↓
🎨 レンダーフェーズ完了
   ↓
🔧 DOM 変更(Mutation Phase)
   ↓
💉 Insertion Effects 実行(コンポーネントごとに Destroy → Create)
   ↓
📐 Layout Effects 実行
   ↓
🖼️ ブラウザ描画
   ↓
⚡ Passive Effects 実行

2.1 エントリポイント: packages/react/src/ReactHooks.js

// packages/react/src/ReactHooks.js

export function useInsertionEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  if (__DEV__) {
    if (create == null) {
      console.warn(
        'React Hook useInsertionEffect requires an effect callback. Did you forget to pass a callback to the hook?',
      );
    }
  }

  const dispatcher = resolveDispatcher();
  return dispatcher.useInsertionEffect(create, deps);
}

引用元: packages/react/src/ReactHooks.js

2.2 コア実装: mountInsertionEffectupdateInsertionEffect

初回レンダー時の処理

// packages/react-reconciler/src/ReactFiberHooks.js

function mountInsertionEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  mountEffectImpl(UpdateEffect, HookInsertion, create, deps);
}

更新時の処理

// packages/react-reconciler/src/ReactFiberHooks.js

function updateInsertionEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return updateEffectImpl(UpdateEffect, HookInsertion, create, deps);
}

引用元: packages/react-reconciler/src/ReactFiberHooks.js

2.3 3つのエフェクトフックの実装比較

3つのエフェクトフックは同じ mountEffectImpl / updateEffectImpl を使用し、フラグだけが異なります

// useEffect
mountEffectImpl(
  PassiveEffect | PassiveStaticEffect,  // Fiber フラグ
  HookPassive,                           // Hook フラグ(0b1000)
  create,
  deps,
);

// useLayoutEffect
mountEffectImpl(
  UpdateEffect | LayoutStaticEffect,    // Fiber フラグ
  HookLayout,                            // Hook フラグ(0b0100)
  create,
  deps,
);

// useInsertionEffect
mountEffectImpl(
  UpdateEffect,                          // Fiber フラグ
  HookInsertion,                         // Hook フラグ(0b0010)
  create,
  deps,
);

Effect フラグの定義

// packages/react-reconciler/src/ReactHookEffectTags.js

export type HookFlags = number;

export const NoFlags = /*   */ 0b0000;
export const HasEffect = /* */ 0b0001;  // Effect を実行すべき
export const Insertion = /* */ 0b0010;  // useInsertionEffect ← ここ!
export const Layout = /*    */ 0b0100;  // useLayoutEffect
export const Passive = /*   */ 0b1000;  // useEffect

引用元: packages/react-reconciler/src/ReactHookEffectTags.js

フック HookFlags ビット値 実行順序
useInsertionEffect Insertion 0b0010 1番目(最初)
useLayoutEffect Layout 0b0100 2番目
useEffect Passive 0b1000 3番目(最後)

2.4 実行タイミング: Mutation Phase での実行

useInsertionEffectMutation Phase(DOM 変更フェーズ)で実行されます。これは useLayoutEffect が実行される Layout Phase よりもです。

// packages/react-reconciler/src/ReactFiberCommitWork.js

case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
  recursivelyTraverseMutationEffects(root, finishedWork, lanes);
  commitReconciliationEffects(finishedWork, lanes);

  if (flags & Update) {
    // 1️⃣ Insertion Effects: Unmount → Mount をコンポーネントごとに実行
    commitHookEffectListUnmount(
      HookInsertion | HookHasEffect,
      finishedWork,
      finishedWork.return,
    );
    commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
    
    // 2️⃣ その後、Layout Effects の Unmount を実行
    commitHookLayoutUnmountEffects(
      finishedWork,
      finishedWork.return,
      HookLayout | HookHasEffect,
    );
  }
  break;
}

引用元: packages/react-reconciler/src/ReactFiberCommitWork.js

2.5 特殊な実行パターン: インターリーブ実行

useInsertionEffect の最大の特徴は、コンポーネントごとにクリーンアップとセットアップが交互に実行されることです。

他のエフェクト(useEffect, useLayoutEffect)の実行順序:

  1. すべてのコンポーネントのクリーンアップを実行
  2. すべてのコンポーネントのセットアップを実行

useInsertionEffect の実行順序(インターリーブ):

  1. コンポーネントAのクリーンアップ → セットアップ
  2. コンポーネントBのクリーンアップ → セットアップ
  3. ...と続く
// 実際のコードでの実行パターン
// コンポーネントごとに Destroy → Create を連続実行
commitHookEffectListUnmount(HookInsertion | HookHasEffect, finishedWork, ...);
commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);

この設計により、CSS-in-JS ライブラリがコンポーネント単位でスタイルを安全に更新できます。

2.6 テストで確認する実行順序

React の公式テストで実行順序が確認されています:

// packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js

it('fires insertion effects before layout effects', async () => {
  let committedText = '(empty)';

  function Counter(props) {
    useInsertionEffect(() => {
      Scheduler.log(`Create insertion [current: ${committedText}]`);
      committedText = String(props.count);
      return () => {
        Scheduler.log(`Destroy insertion [current: ${committedText}]`);
      };
    });
    useLayoutEffect(() => {
      Scheduler.log(`Create layout [current: ${committedText}]`);
      return () => {
        Scheduler.log(`Destroy layout [current: ${committedText}]`);
      };
    });
    useEffect(() => {
      Scheduler.log(`Create passive [current: ${committedText}]`);
      return () => {
        Scheduler.log(`Destroy passive [current: ${committedText}]`);
      };
    });
    return null;
  }

  await act(async () => {
    ReactNoop.render(<Counter count={0} />);
    await waitForPaint([
      'Create insertion [current: (empty)]',  // 1️⃣ Insertion が最初
      'Create layout [current: 0]',            // 2️⃣ Layout が次
    ]);
  });

  assertLog(['Create passive [current: 0]']);  // 3️⃣ Passive が最後
});

引用元: packages/react-reconciler/src/tests/ReactHooksWithNoopRenderer-test.js

2.7 複数コンポーネントでの実行順序

複数コンポーネントがある場合の実行順序も、テストで確認されています:

// 初回マウント時の実行順序
await waitForAll([
  // すべての Insertion Effects が先に実行
  'Create Insertion 1 for Component A [A: (empty), B: (empty)]',
  'Create Insertion 2 for Component A [A: 0, B: (empty)]',
  'Create Insertion 1 for Component B [A: 0, B: (empty)]',
  'Create Insertion 2 for Component B [A: 0, B: 0]',
  // その後、すべての Layout Effects が実行
  'Create Layout 1 for Component A [A: 0, B: 0]',
  'Create Layout 2 for Component A [A: 0, B: 0]',
  'Create Layout 1 for Component B [A: 0, B: 0]',
  'Create Layout 2 for Component B [A: 0, B: 0]',
]);

// 更新時の実行順序(インターリーブ)
await waitForAll([
  // Component A: Insertion の Destroy → Create
  'Destroy Insertion 1 for Component A [A: 0, B: 0]',
  'Destroy Insertion 2 for Component A [A: 0, B: 0]',
  'Create Insertion 1 for Component A [A: 0, B: 0]',
  'Create Insertion 2 for Component A [A: 1, B: 0]',
  // Component A: Layout の Destroy
  'Destroy Layout 1 for Component A [A: 1, B: 0]',
  'Destroy Layout 2 for Component A [A: 1, B: 0]',
  // Component B: Insertion の Destroy → Create
  'Destroy Insertion 1 for Component B [A: 1, B: 0]',
  'Destroy Insertion 2 for Component B [A: 1, B: 0]',
  'Create Insertion 1 for Component B [A: 1, B: 0]',
  'Create Insertion 2 for Component B [A: 1, B: 1]',
  // Component B: Layout の Destroy
  'Destroy Layout 1 for Component B [A: 1, B: 1]',
  'Destroy Layout 2 for Component B [A: 1, B: 1]',
  // すべての Layout の Create
  'Create Layout 1 for Component A [A: 1, B: 1]',
  'Create Layout 2 for Component A [A: 1, B: 1]',
  'Create Layout 1 for Component B [A: 1, B: 1]',
  'Create Layout 2 for Component B [A: 1, B: 1]',
]);

引用元: packages/react-reconciler/src/tests/ReactHooksWithNoopRenderer-test.js

3. ユースケース

3.1 CSS-in-JS ライブラリでの動的スタイル注入

useInsertionEffect唯一の推奨ユースケースは、CSS-in-JS ライブラリ内でのスタイル挿入です。

// CSS-in-JS ライブラリの内部実装例
let isInserted = new Set();

function useCSS(rule) {
  useInsertionEffect(() => {
    // 同じルールを重複して挿入しないようにチェック
    if (!isInserted.has(rule)) {
      isInserted.add(rule);
      
      // <style> タグを作成して DOM に挿入
      const styleElement = document.createElement('style');
      styleElement.textContent = rule;
      document.head.appendChild(styleElement);
    }
  });
  
  return rule;
}

// 使用側コンポーネント
function Button() {
  const className = useCSS(`
    .my-button {
      background: blue;
      color: white;
      padding: 10px 20px;
    }
  `);
  
  return <button className="my-button">Click me</button>;
}

3.2 サーバーサイドレンダリング対応

useInsertionEffect はクライアントサイドでのみ実行されます。SSR 時に使用された CSS ルールを収集するには、レンダー中に行います:

let collectedRulesSet = new Set();

function useCSS(rule) {
  // サーバー側: レンダー中にルールを収集
  if (typeof window === 'undefined') {
    collectedRulesSet.add(rule);
  }
  
  // クライアント側: エフェクトでスタイルを挿入
  useInsertionEffect(() => {
    if (!isInserted.has(rule)) {
      isInserted.add(rule);
      const style = document.createElement('style');
      style.textContent = rule;
      document.head.appendChild(style);
    }
  });
  
  return rule;
}

// SSR 時に収集したルールを取得
function getCollectedCSS() {
  return Array.from(collectedRulesSet).join('\n');
}

3.3 クリーンアップ付きスタイル管理

コンポーネントがアンマウントされたときにスタイルを削除する例:

function useDynamicStyle(css) {
  useInsertionEffect(() => {
    const style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);
    
    // クリーンアップ: コンポーネントがアンマウントされたらスタイルを削除
    return () => {
      document.head.removeChild(style);
    };
  }, [css]);
}

function ThemeProvider({ theme, children }) {
  useDynamicStyle(`
    :root {
      --primary-color: ${theme.primaryColor};
      --secondary-color: ${theme.secondaryColor};
    }
  `);
  
  return children;
}

4. なぜ useLayoutEffect ではなく useInsertionEffect なのか

4.1 パフォーマンスの違い

レンダー中にスタイルを挿入するのは非常に遅いです。ブラウザはスタイルを挿入するたびに、すべての CSS ルールを再計算する必要があるためです。

// ❌ 非常に遅い: レンダー中にスタイルを挿入
function Component() {
  // レンダーのたびに実行される!
  const style = document.createElement('style');
  document.head.appendChild(style);
  return <div />;
}

// ⚠️ 遅い: useLayoutEffect でスタイルを挿入(他の測定と競合する可能性)
function Component() {
  useLayoutEffect(() => {
    const style = document.createElement('style');
    document.head.appendChild(style);
  }, []);
  return <div />;
}

// ✅ 推奨: useInsertionEffect でスタイルを挿入
function Component() {
  useInsertionEffect(() => {
    // レイアウト測定の前に実行される
    const style = document.createElement('style');
    document.head.appendChild(style);
  }, []);
  return <div />;
}

4.2 実行順序の保証

useInsertionEffect を使えば、すべての useLayoutEffect より前にスタイルが挿入されることが保証されます。これにより、レイアウト測定時にはスタイルが確実に適用されています。

5. よくある間違いと注意点

5.1 一般的なアプリケーションでの使用

// ❌ 間違い: 一般的な副作用に useInsertionEffect を使用
function Component() {
  useInsertionEffect(() => {
    // データ取得などには使わない!
    fetchData();
  }, []);
}

// ✅ 正しい: useEffect を使用
function Component() {
  useEffect(() => {
    fetchData();
  }, []);
}

5.2 state の更新

// ❌ 間違い: useInsertionEffect 内で state を更新
function Component() {
  const [style, setStyle] = useState('');
  
  useInsertionEffect(() => {
    setStyle('color: red');  // これは動作しない!
  }, []);
}

5.3 ref へのアクセス

// ❌ 間違い: useInsertionEffect 内で ref にアクセス
function Component() {
  const ref = useRef(null);
  
  useInsertionEffect(() => {
    console.log(ref.current);  // null の可能性が高い
  }, []);
  
  return <div ref={ref} />;
}

6. まとめ

6.1 useInsertionEffect を使うべき場面

場面 使用すべきか
CSS-in-JS ライブラリの開発 ✅ はい
動的スタイルの挿入 ✅ はい
通常のアプリケーション開発 ❌ いいえ
DOM 測定 ❌ いいえ(useLayoutEffect を使用)
データ取得 ❌ いいえ(useEffect を使用)

6.2 エフェクトフックの選び方

6.3 実装のポイント

  1. useInsertionEffect は CSS-in-JS 専用: 一般的なアプリケーションでは使用しない
  2. state 更新禁止: エフェクト内で setState を呼び出さない
  3. ref アクセス不可: 実行時点では ref がアタッチされていない
  4. クリーンアップを忘れずに: 挿入したスタイルは適切に削除する
  5. SSR 対応: サーバー側ではレンダー中にルールを収集する

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?