React で「副作用」といえば useEffect を思い浮かべる方が多いでしょう。しかし、こんな経験はありませんか?
// ツールチップの位置を計算したい
useEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setPosition(calculatePosition(height));
}, []);
// → なぜかツールチップが一瞬「ジャンプ」する...? 🤔
「useEffect でいいや」 と思考停止していませんか?
実は React には、実行タイミングが異なるエフェクトフックがあります。それぞれの特性を理解し、適切に使い分けることで、ちらつきのない滑らかな UI や、意図しない再実行のないエフェクトを実現できます。
本記事では、useEffect・useLayoutEffect・useEffectEvent の 3 つを徹底比較。「いつ」「なぜ」「どう使い分けるか」を解説します!
📝 検証環境: この記事は facebook/react リポジトリ(2025年12月時点の main ブランチ)の実際のソースコードを参照して書かれています。
💡 エフェクトとは?
エフェクト(副作用)を使うことで、コンポーネントを外部システムに接続し、同期させることができます。これには、ネットワーク、ブラウザの DOM、アニメーション、別の UI ライブラリを使って書かれたウィジェット、その他の非 React コードの処理が含まれます。
1. 3 つのエフェクトフック概要比較
まずは一覧で全体像を把握しましょう。それぞれがどんな目的で使われ、いつ実行されるのかを理解することで、適切なフックを選ぶ第一歩になります。
| フック | 実行タイミング | 主な用途 | ブロッキング |
|---|---|---|---|
| useEffect | ブラウザ描画後(非同期) | API呼び出し、イベントリスナー、タイマー | しない |
| useLayoutEffect | ブラウザ描画前(同期) | DOM測定、レイアウト計算 | する |
| useEffectEvent | エフェクト内で呼び出し時 | 依存配列に追加せず最新値を取得 | - |
エフェクトフックの実行順序
フックの分類
2. useEffect — 標準的な副作用処理
2.1 役割
useEffect は レンダリング後、ブラウザ描画後に副作用を非同期で実行するためのフックです。最も一般的なエフェクトフックであり、ほとんどの副作用処理はこれで対応できます。
2.2 基本的な使い方
import { useState, useEffect } from 'react';
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
// セットアップ: 接続を開始
const connection = createConnection(roomId);
connection.connect();
connection.on('message', (msg) => {
setMessages((prev) => [...prev, msg]);
});
// クリーンアップ: 接続を解除
return () => {
connection.disconnect();
};
}, [roomId]); // 依存配列: roomId が変わったら再実行
return <MessageList messages={messages} />;
}
2.3 Hookの使用
useEffect(setup, dependencies?)
| 引数 | 説明 |
|---|---|
setup |
副作用のロジックを含む関数。クリーンアップ関数を返すことができる |
dependencies |
(オプション) 依存配列。この値が変わった時にエフェクトが再実行される |
2.4 依存配列のパターン
// パターン1: 依存配列あり → 依存値が変わった時のみ再実行
useEffect(() => {
fetchData(userId);
}, [userId]);
// パターン2: 空の依存配列 → マウント時のみ実行
useEffect(() => {
initializeAnalytics();
return () => cleanupAnalytics();
}, []);
// パターン3: 依存配列なし → 毎回のレンダー後に実行(通常は避ける)
useEffect(() => {
document.title = `Count: ${count}`;
});
依存配列とは
依存配列とは、エフェクト内で使用するpropsやstateを依存配列に追加することで、エフェクト内で使用するpropsやstateが変更されたときにエフェクトを再実行することができるものです。
例えば、以下のようなエフェクトがあるとします。
useEffect(() => {
console.log(count);
}, [count]);
このエフェクトは、countが変更されたときに再実行されます。
しかし、以下のようなエフェクトがあるとします。
useEffect(() => {
console.log(count);
}, [count]);
このエフェクトは、countが変更されたときに再実行されません。
なぜなら、依存配列にcountが含まれていないからです。
useEffect(() => {
console.log(count);
}, []);
このエフェクトは、countが変更されたときに再実行されません。
useEffect(() => {
console.log(count);
}, [count]);
このエフェクトは、countが変更されたときに再実行されます。
useEffect(() => {
console.log(count);
}, [count]);
このエフェクトは、countが変更されたときに再実行されます。
2.5 useEffect の適切な使用場面
| ✅ 適している | ❌ 適していない |
|---|---|
| API からのデータ取得 | DOM のサイズ測定(ちらつく) |
| イベントリスナーの登録 | 同期的な DOM 操作 |
| WebSocket 接続 | スタイルの動的挿入 |
| タイマーの設定 | レイアウト計算 |
| 外部ライブラリの初期化 | - |
2.6 よくある使用例
API からのデータ取得
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (!cancelled) {
setUser(data);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchUser();
return () => {
cancelled = true;
};
}, [userId]);
if (loading) return <p>Loading...</p>;
return <p>{user?.name}</p>;
}
イベントリスナーの登録
function WindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight });
};
handleResize(); // 初期値を設定
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return <p>{size.width} x {size.height}</p>;
}
💡 クリーンアップ関数の重要性
イベントリスナーやタイマーなどのリソースを使用する場合、必ずクリーンアップ関数で解除してください。クリーンアップを忘れると、メモリリークや予期しない動作の原因になります。
2.7 facebook/react での実装
facebook/react リポジトリを確認すると、useEffect は HookPassive フラグを使用しています。
// packages/react-reconciler/src/ReactFiberHooks.js
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect, // Fiber フラグ
HookPassive, // Hook フラグ
create,
deps,
);
}
PassiveEffect フラグにより、エフェクトはブラウザ描画後に非同期で実行されます。
3. useLayoutEffect — ブラウザ描画前の同期処理
3.1 役割
useLayoutEffect は ブラウザが画面を描画する前に同期的に実行されるフックです。DOM のサイズや位置を測定し、それに基づいて UI を更新する場合に使用します。
3.2 useEffect との違い
| 観点 | useEffect | useLayoutEffect |
|---|---|---|
| 実行タイミング | ブラウザ描画後 | ブラウザ描画前 |
| 同期/非同期 | 非同期 | 同期 |
| ブロッキング | しない | する |
| ちらつき | 発生する可能性あり | 発生しない |
3.3 なぜ useLayoutEffect が必要か
問題のケース: ツールチップを要素の上か下に表示したい
useEffect を使う場合:
- ツールチップを仮の位置でレンダー
- ブラウザが画面を描画(ユーザーが見える)
- 高さを測定
- 正しい位置で再レンダー
- ブラウザが再描画
結果: ユーザーにはツールチップが「ジャンプ」して見える(ちらつき)
useLayoutEffect を使う場合:
- ツールチップを仮の位置でレンダー
- 高さを測定(描画前)
- 正しい位置で再レンダー
- ブラウザが画面を描画(ユーザーに見えるのはここだけ)
結果: ユーザーには最終的な正しい位置だけが見える
3.4 基本的な使い方
import { useLayoutEffect, useRef, useState } from 'react';
function Tooltip({ children, targetRect }) {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);
useLayoutEffect(() => {
// ブラウザが画面を描画する前に実行される
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height);
}, []);
// tooltipHeight を使って位置を計算
let tooltipY = targetRect.top - tooltipHeight;
if (tooltipY < 0) {
// 上に収まらない場合は下に表示
tooltipY = targetRect.bottom;
}
return (
<div
ref={ref}
style={{ position: 'absolute', top: tooltipY }}
>
{children}
</div>
);
}
3.5 Hookの使用
useLayoutEffect(setup, dependencies?)
| 引数 | 説明 |
|---|---|
setup |
副作用のロジックを含む関数。クリーンアップ関数を返すことができる |
dependencies |
(オプション) 依存配列。この値が変わった時にエフェクトが再実行される |
💡 useEffect と同じ API
useLayoutEffect は useEffect と全く同じ引数・返り値を持ちます。違いは実行タイミングだけです。そのため、必要に応じて簡単に切り替えられます。
3.6 useLayoutEffect の適切な使用場面
| ✅ 適している | ❌ 適していない |
|---|---|
| DOM のサイズ・位置の測定 | API 呼び出し |
| ツールチップの位置計算 | イベントリスナーの登録 |
| スクロール位置の復元 | 外部ライブラリの初期化 |
| アニメーションの初期設定 | 重い計算処理 |
⚠️ パフォーマンスに関する注意
useLayoutEffect はブラウザの描画をブロックするため、過度に使用するとアプリのパフォーマンスが低下します。可能な限り useEffect を使用し、DOM 測定など本当に必要な場合のみ useLayoutEffect を使用してください。
3.7 実行順序の可視化
3.8 facebook/react での実装
useLayoutEffect は HookLayout フラグを使用しています。
// packages/react-reconciler/src/ReactFiberHooks.js
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
let fiberFlags: Flags = UpdateEffect | LayoutStaticEffect;
if (
__DEV__ &&
(currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
) {
fiberFlags |= MountLayoutDevEffect;
}
return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}
💡 useEffect との違い
useEffect は HookPassive フラグを使用し、ブラウザ描画後に非同期で実行されます。一方、useLayoutEffect は HookLayout フラグを使用し、ブラウザ描画前に同期的に実行されます。
// packages/react-reconciler/src/ReactFiberHooks.js
function mountEffect(
// useEffect の場合(mountEffect関数内)
mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
function mountLayoutEffect(
// useLayoutEffect の場合(mountLayoutEffect関数内)
mountEffectImpl(fiberFlags, HookLayout, create, deps);
この違いにより、useLayoutEffect は DOM の測定や同期的な更新が可能になりますが、描画をブロックするためパフォーマンスへの影響に注意が必要です。
4. useEffectEvent — 非リアクティブなロジックの抽出
4.1 役割
useEffectEvent は React 18.3 で追加されたフックで、エフェクトから非リアクティブなロジックを抽出するために設計されています。このフックを使うと、props や state の最新値を読み取りながらも、それらの変化でエフェクトを再実行させないようにできます。
4.2 なぜ useEffectEvent が必要か
問題のケース:
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => {
// ❌ 問題: theme を使うと依存配列に追加が必要
showNotification('Connected!', theme);
});
connection.connect();
return () => connection.disconnect();
}, [roomId, theme]); // theme が変わるたびに再接続してしまう!
}
問題: theme を依存配列に追加すると、テーマが変わるたびにチャットルームへの再接続が発生してしまいます。しかし、通知を表示するために theme の最新値は必要です。
解決策:
function ChatRoom({ roomId, theme }) {
// ✅ エフェクトイベントを定義
const onConnected = useEffectEvent(() => {
// theme の最新値を読み取れるが、エフェクトの依存値にはならない
showNotification('Connected!', theme);
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => {
onConnected(); // エフェクトイベントを呼び出す
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // theme は依存配列に不要!
}
結果: roomId が変わった時のみ再接続し、theme が変わっても再接続は発生しません。しかし通知は常に最新の theme で表示されます。
4.3 Hookの使用
const onSomething = useEffectEvent(callback)
| 引数 | 説明 |
|---|---|
callback |
エフェクトイベントのロジックを含む関数。呼び出された瞬間の props や state の最新値にアクセスできる |
返り値: エフェクトイベント関数。この関数は useEffect や useLayoutEffect 内で呼び出すことができます。
4.4 リアクティブな値 vs 非リアクティブな値
💡 リアクティブな値とは?
リアクティブな値とは、コンポーネントのレンダー中に計算され、React のデータフローに参加する値のことです。具体的には以下が該当します:
- props: 親コンポーネントから渡される値
-
state:
useStateやuseReducerで管理する値 - 派生値: props や state から計算される値(コンポーネント本体で宣言された変数)
これらの値が変わると、コンポーネントが再レンダーされ、useEffect の依存配列に含まれている場合はエフェクトも再実行されます。
4.5 重要な制約
useEffectEvent には厳格な使用ルールがあります:
| 制約 | 理由 |
|---|---|
| ❌ エフェクト外で呼び出せない | レンダー中に呼び出すとエラーになる |
| ❌ 他のコンポーネントに渡せない | 安定した参照が保証されないため |
| ❌ 依存配列を避けるためだけに使わない | バグが隠蔽され、コードが理解しにくくなる |
| ✅ 非リアクティブなロジック専用 | 値の変化に依存しないロジックのみ |
function Component() {
const onClick = useEffectEvent(() => {
console.log('clicked');
});
// ❌ レンダー中に呼び出すとエラー!
onClick();
// ✅ エフェクト内で呼び出す
useEffect(() => {
document.addEventListener('click', onClick);
return () => document.removeEventListener('click', onClick);
}, []);
return <button>Click</button>;
}
4.6 useEffectEvent の適切な使用場面
| ✅ 適している | ❌ 適していない |
|---|---|
| エフェクト内で最新の props/state を読み取りたい | 依存配列を減らしたいだけ |
| 値の変化でエフェクトを再実行したくない | レンダー中に呼び出したい |
| イベントハンドラをエフェクトから分離したい | 他のコンポーネントに渡したい |
4.7 よくある使用例
最新の設定値でログを記録
function Analytics({ page, settings }) {
const logVisit = useEffectEvent(() => {
// settings の最新値でログを記録
sendAnalytics(page, settings.userId, settings.sessionId);
});
useEffect(() => {
logVisit();
}, [page]); // page が変わった時のみ実行、settings は不要
}
アニメーション完了時の通知
function AnimatedComponent({ item, theme }) {
const onAnimationEnd = useEffectEvent(() => {
// theme の最新値で通知
showToast(`Animation completed!`, theme);
});
useEffect(() => {
const element = ref.current;
element.addEventListener('animationend', onAnimationEnd);
return () => element.removeEventListener('animationend', onAnimationEnd);
}, [item]); // item が変わった時のみリスナーを再設定
}
4.8 facebook/react での実装
// packages/react-reconciler/src/ReactFiberHooks.js
function mountEvent<Args, Return, F: (...Array<Args>) => Return>(
callback: F,
): F {
const hook = mountWorkInProgressHook();
const ref = {impl: callback};
hook.memoizedState = ref;
return function eventFn() {
if (isInvalidExecutionContextForEventFunction()) {
throw new Error(
"A function wrapped in useEffectEvent can't be called during rendering.",
);
}
return ref.impl.apply(undefined, arguments);
};
}
ポイント:
-
refオブジェクトを作成し、implプロパティに callback を保存 - 返される関数は、呼び出されるたびに
ref.implを実行 - レンダー中に呼び出されるとエラーをスロー
5. 選択フローチャート
「どのエフェクトフックを使うべきか」を判断するためのフローチャートです。迷ったときはここに戻ってきてください!
各フックの判断基準まとめ
| シチュエーション | 選択するフック |
|---|---|
| API からデータを取得したい | useEffect |
| イベントリスナーを登録したい | useEffect |
| タイマーを設定したい | useEffect |
| 外部ライブラリを初期化したい | useEffect |
| DOM のサイズを測定したい(ちらつき防止) | useLayoutEffect |
| スクロール位置を復元したい | useLayoutEffect |
| ツールチップの位置を計算したい | useLayoutEffect |
| 最新の props/state を読み取りたいが依存配列に追加したくない | useEffectEvent |
6. 比較表と使い分けガイド
6.1 3つのエフェクトフック総合比較
| 項目 | useEffect | useLayoutEffect | useEffectEvent |
|---|---|---|---|
| 実行タイミング | 描画後(非同期) | 描画前(同期) | 呼び出し時 |
| ブロッキング | しない | する | - |
| クリーンアップ | あり | あり | なし |
| 依存配列 | あり | あり | なし |
| state 更新 | ✅ | ✅ | ✅ |
| ref アクセス | ✅ | ✅ | ✅ |
| 主な対象 | 全ての開発者 | 全ての開発者 | 全ての開発者 |
6.2 使い分け早見表
📊 データ取得・外部連携 → useEffect
📐 DOM 測定・レイアウト → useLayoutEffect
🎯 非リアクティブロジック → useEffectEvent + useEffect
💡 データフェッチングには専用ライブラリを検討しよう
useEffect でのデータ取得は多くの問題(キャッシュなし、重複リクエスト、Race Condition など)を抱えています。TanStack Query や SWR などの専用ライブラリを使うことで、これらの問題を解決できます。
まとめ
本記事で学んだ 3 つのエフェクトフックを一言でまとめると、以下のようになります。
| フック | 一言でまとめると |
|---|---|
| useEffect | 「描画後に非同期で副作用を実行」 |
| useLayoutEffect | 「描画前に同期で DOM 測定・操作」 |
| useEffectEvent | 「依存配列なしで最新値にアクセス」 |
これらのエフェクトフックを適切に使い分けることで、ちらつきのない滑らかな UI、意図しない再実行のないエフェクト、そしてメンテナンスしやすいコードを実現できます。