はじめに
非同期処理をuseEffect内で書いていると、こんな問題に遭遇したことはありませんか?
古いリクエストのレスポンスが後から返ってきて、UIが古い状態で上書きされてしまう
このような 非同期の競合(race condition) は、実は useEffect を使ったネットワーク通信でよく起きる落とし穴です。
この記事では、この問題を解決するための超実用的テクニックである ignore フラグと useEffect の cleanup 関数の仕組みを徹底解説します。
問題
ステートが「古いレスポンス」で上書きされる
以下はポケモンAPIを使ったカルーセルの一部実装例です:
useEffect(() => {
async function fetchData() {
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
const data = await res.json();
setPokemon(data); // ← ここで古いデータが上書きされる可能性あり
}
fetchData();
}, [id]);
一見よさそうですが、例えば次のような状況を想像してみてください:
- id=1 の時に
fetch開始(fetching中) -
すぐに「次へ」ボタンを押して、APIを
id=2に変更 -
id=2のリクエストが先に終わり、id=2の結果を表示 - しかしその後に
id=1のレスポンスが帰ってきて上書きされてしまう
🤔結果:UIが古いポケモンを表示してしまう
解決法
ignoreフラグで古いリクエストを無視する
Reactの useEffect には「クリーンアップ関数」という機能があります。これは、次の useEffect が走る直前やコンポーネントがアンマウントされる直前に呼ばれます。
この性質を利用して、こうします:
useEffect(() => {
let ignore = false;
async function fetchData() {
setLoading(true);
setError(null);
const { response, error } = await fetchPokemon(id);
if (!ignore) {
if (error) setError(error);
else setPokemon(response);
setLoading(false);
}
}
fetchData();
return () => {
ignore = true;
};
}, [id]);
💡 解説
✅ 1. ignore はクロージャで閉じている
-
ignoreフラグは このuseEffectがまだ有効かどうか を表す -
useEffect内のignoreはその時点でのスコープに閉じている - 後から別の
useEffectが実行されても、前のignoreがtrueになるだけで、それに依存した処理は止まる
✅ 2. ReactはuseEffectを順番通りに実行&クリーンアップ
- 次の
useEffect(id = 2) が走ると、前のuseEffect(id = 1)はignore = trueになる - そのため、後から返ってきた古いレスポンスは
ignore === trueによって無視される
✅ 3. 状態更新は最新のリクエストだけが行う
- 副作用が発生する前に
ignoreをチェックすることで、UIが誤って古い状態に戻ることを防げます。 - つまり、古いリクエストのレスポンスを無視できる
📌 なぜキャンセルではなく「無視」なのか?
fetch は中断(abort)できますが、より簡単に安全性を保ちたい場合、「レスポンスを無視する」という戦略のほうが実装が楽です。
- キャンセルAPI(
AbortController)を使うと少し複雑 -
ignoreフラグは 副作用の発生を防ぐ だけなので手軽
📄 ignore パターンのテンプレート
useEffect(() => {
let ignore = false;
async function fetchData() {
const result = await someAsyncFunction();
if (!ignore) {
// state更新など
}
}
fetchData();
return () => {
ignore = true;
};
}, [dependencies]);
これはAPI通信以外でも、非同期処理の副作用がある全てのケースに使えます。
cleanup function の正体とは?
useEffect に return された関数のこと。
React はこれを 次の effect 実行前 または コンポーネントがアンマウントされる直前 に実行します。
useEffect(() => {
// 副作用の実行
startSubscription();
// 👇 これが cleanup function
return () => {
stopSubscription(); // 後始末
};
}, [deps]);
🕒 React内部での処理タイミング
React の実行タイミングは以下の通り:
- 初回レンダリング → effect 実行
-
depsの変更 → 前の cleanup → 新しい effect 実行 - アンマウント時 → 最後の cleanup 実行
🧪 どんなときに使うの?
| 使用ケース | cleanup の役割 |
|---|---|
| イベントリスナーの登録 | 削除する(removeEventListener) |
| WebSocket の接続 | 切断する(socket.close()) |
setInterval / setTimeout
|
clearInterval / clearTimeout
|
| fetch の競合防止 |
ignore = true のようなフラグセット |
| サブスクリプション | 解除処理を呼ぶ |
まとめ
| 問題 | 解決 |
|---|---|
| 非同期レスポンスが競合してUIが壊れる |
ignore フラグで古いリクエストを無視する |
- 非同期処理は「競合」によって意図しないUI更新を起こすことがある
-
ignoreは**現在の effect が「まだ有効か」**を判断するためのフラグ -
useEffectのクリーンアップ関数内でignore = trueにするだけでOK -
ignoreフラグとuseEffectのクリーンアップを組み合わせれば安全に防げる - クロージャ+Reactの実行順で古いリクエストを安全に無視できる
- 複雑なキャンセル処理なしで 副作用の安全性を確保できる
非同期の副作用に悩んでいる方は、ぜひこのパターンを取り入れてみてください!