Reactの現場で「なぜかuseEffectが二回動く」「思ったタイミングで同期が取れない」という壁にぶつかったことはないだろうか?
かつてのReactは「描画したから、副作用(effect)を実行する」というシンプルな同期の世界にいた。しかし、React 18以降のConcurrent React(並行レンダリング)の世界では、その前提は崩れ去ったんだ。今日は、中級者が必ず一度はハマる「Concurrent環境下でのuseEffectの真実」について、現場の知見を交えて深掘りしていくぞ。
—
1. 「レンダリング ≠ 画面描画」という現実
まず、頭を切り替えてほしい。ConcurrentモードにおけるReactは、「レンダリング(関数の実行とVirtual DOMの計算)」と「コミット(DOMへの反映)」を完全に分離している。
Reactは、より優先度の高い処理(ユーザーの入力など)があれば、進行中のレンダリングを一時停止したり、捨てたり、再開したりする。これこそが「中断・再開」の正体だ。
useEffectはいつ動くのか?
Reactの仕様上、`useEffect`は「コミットが完了し、ブラウザが画面をペイントした後」に実行される。
つまり、Reactが内部で計算を何度もやり直していても、ユーザーの目に触れるDOMの更新が確定した後にしか、副作用のターンは回ってこないようになっている。これは賢い仕組みだが、同時に「非同期処理との付き合い方」には細心の注意が必要になるということだ。
—
2. 現場で一番怖い「レースコンディション」
Concurrent Reactで最も悲劇を生むのが、副作用の実行タイミングが「前後する」ことだ。
例えば、ユーザーが素早くタブを切り替えたとき。古いリクエストが完了する前に新しいリクエストが始まると、古い結果で画面が上書きされてしまう。これを防ぐのが「クリーンアップ関数」の役割だ。
実践:キャンセル可能な非同期処理のパターン
現場で一番安全で、かつ綺麗な実装パターンを提示する。`useEffect`の中で非同期関数を実行する際は、必ず「この副作用がまだ有効か?」をフラグで管理してほしい。
import { useState, useEffect } from ‘react’;
const UserProfile = ({ userId }) => {
const [data, setData] = useState(null);
useEffect(() => {
// 1. 副作用が「有効」であることを示すフラグを用意
let isCancelled = false;
const fetchData = async () => {
try {
const response = await fetch(`/api/users/${userId}`);
const result = await response.json();
// 2. Reactが中断・再開を繰り返した後、
// 最後に実行された副作用以外は無視する
if (!isCancelled) {
setData(result);
}
} catch (error) {
if (!isCancelled) console.error(error);
}
};
fetchData();
// 3. コンポーネントがアンマウント、または依存配列が更新されたら
// クリーンアップ関数が走る
return () => {
isCancelled = true;
};
}, [userId]);
return
;
};
このコードの肝は、`isCancelled` というローカル変数にある。クリーンアップ関数が呼ばれた時点で、その副作用は「過去のもの」として握りつぶす。これがConcurrent Reactにおける鉄則だ。
—
3. なぜ「Strict Mode」で二回動くのか?
開発環境で`useEffect`が二回動くことに苛立っている人は多いはずだ。あれはバグではなく、「コンポーネントが純粋(Pure)であること」を強制的にテストする仕組みだ。
Reactは、「コンポーネントが何回レンダリングされても、副作用が正しくクリーンアップされ、整合性が保たれるか」を確認している。つまり、二回動いて困るような副作用(APIを二回叩いて整合性が崩れる等)は、設計が間違っているとReactが警告してくれているんだ。
現場の知恵:副作用の責務を分離せよ
「useEffectの中で何でもやりすぎないこと」。これが一番の処方箋だ。
1. データ取得: `useSWR` や `TanStack Query (React Query)` を使え。これらはキャッシュとレースコンディションの管理をライブラリ側で完璧に処理してくれる。
2. イベントハンドラ: 副作用のトリガーがユーザーの操作なら、`useEffect`ではなくイベントハンドラ内で完結させる。
3. 同期: DOMの直接操作や外部ライブラリとの連携など、どうしても`useEffect`が必要な場合のみに絞る。
—
まとめ:アーキテクトからのアドバイス
Concurrent Reactは、Reactを「UIを描画するだけのライブラリ」から「状態を時間軸で管理するオーケストレーター」へと進化させた。
- useEffectは「魔法の箱」ではない。 単なる「コミット後のフック」に過ぎない。
- クリーンアップ関数を書き忘れるな。 それは「後始末」ではなく、「非同期の連鎖を止めるためのブレーキ」だ。
- ライブラリを信頼せよ。 データ取得で `useEffect` を書いている時点で、現代のReact開発としては「泥臭い」領域だ。賢いツールに任せて、自分はコンポーネントのロジックに集中すべきだ。
もし今、君のプロジェクトで`useEffect`がスパゲッティ化しているなら、それは「副作用の責務」をコンポーネントが持ちすぎているサインかもしれない。一度立ち止まって、その副作用が本当にそこにあるべきか、問い直してみてほしい。
現場からは以上だ。次は、`useLayoutEffect` との使い分けについて話そうか。また現場で会おう。

コメント