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);
}
2.2 コア実装: mountInsertionEffect と updateInsertionEffect
初回レンダー時の処理
// 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);
}
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
| フック | HookFlags | ビット値 | 実行順序 |
|---|---|---|---|
useInsertionEffect |
Insertion |
0b0010 |
1番目(最初) |
useLayoutEffect |
Layout |
0b0100 |
2番目 |
useEffect |
Passive |
0b1000 |
3番目(最後) |
2.4 実行タイミング: Mutation Phase での実行
useInsertionEffect は Mutation 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;
}
2.5 特殊な実行パターン: インターリーブ実行
useInsertionEffect の最大の特徴は、コンポーネントごとにクリーンアップとセットアップが交互に実行されることです。
他のエフェクト(useEffect, useLayoutEffect)の実行順序:
- すべてのコンポーネントのクリーンアップを実行
- すべてのコンポーネントのセットアップを実行
useInsertionEffect の実行順序(インターリーブ):
- コンポーネントAのクリーンアップ → セットアップ
- コンポーネントBのクリーンアップ → セットアップ
- ...と続く
// 実際のコードでの実行パターン
// コンポーネントごとに 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 実装のポイント
- useInsertionEffect は CSS-in-JS 専用: 一般的なアプリケーションでは使用しない
-
state 更新禁止: エフェクト内で
setStateを呼び出さない - ref アクセス不可: 実行時点では ref がアタッチされていない
- クリーンアップを忘れずに: 挿入したスタイルは適切に削除する
- SSR 対応: サーバー側ではレンダー中にルールを収集する