Reactの闇を切り裂く:非同期レースコンディションを「クリーンアップ関数」で制圧する
フロントエンドの戦場において、最も卑劣で、かつ最も見落とされがちなバグ。それが「レースコンディション(競合状態)」です。
例えば、ユーザーが検索窓に素早く文字を入力し、そのたびにAPIリクエストが飛ぶ状況を想像してください。ネットワークの遅延は気まぐれです。「遅く送ったリクエストが先に届き、早く送ったリクエストが後から上書きする」という、いわゆる「後出しジャンケン」による状態の不整合。これに気づかず放置しているのは、時限爆弾を抱えたままプロダクション環境へコードをデプロイしているのと同じです。
今回は、Reactの `useEffect` を単なる「副作用の入れ物」から、高度な「状態制御のゲートキーパー」へと昇華させる技術を伝授します。
—
なぜ「直感的なコード」が破滅を招くのか
多くのエンジニアが最初に書く、危険なパターンを見てみましょう。
useEffect(() => {
let isMounted = true; // 心理的な安心感のためのフラグ(これだけでは不十分)
const fetchData = async () => {
const data = await fetchApi(query);
if (isMounted) {
setData(data); // 競合が起きると、古いリクエストの結果で上書きされる
}
};
fetchData();
return () => { isMounted = false; };
}, [query]);
このコードの問題点は、`isMounted` フラグが「コンポーネントが破棄されたかどうか」しか見ていない点にあります。`query` が頻繁に変更される場合、複数の非同期処理が同時に走り、すべてが成功して `setData` を叩いてしまう。これでは、最後に表示されるべきデータが保証されません。
究極の解法:クリーンアップ関数による「直前リクエストの無効化」
真に堅牢なアーキテクチャを目指すなら、「現在のエフェクト実行中のみが、状態を書き換える権利を持つ」という排他制御の原則を徹底すべきです。
クリーンアップ関数は、Reactが「次の副作用を開始する直前」や「コンポーネントアンマウント時」に必ず実行されるという性質を持っています。これを利用し、リクエストごとに一意のフラグを制御します。
import { useEffect, useState } from ‘react’;
const SearchResult = ({ query }) => {
const [data, setData] = useState(null);
useEffect(() => {
// このエフェクトの生存期間を制御するフラグ
let isCurrent = true;
const performAsyncOperation = async () => {
try {
const result = await fetchApi(query);
// 重要: 自分のエフェクトがまだ「最新」である場合のみ更新する
if (isCurrent) {
setData(result);
}
} catch (err) {
if (isCurrent) console.error(“API Error”, err);
}
};
performAsyncOperation();
// クリーンアップ関数: 次の副作用が走る前に、前の処理を「無効化」する
return () => {
isCurrent = false;
};
}, [query]); // queryが更新されるたびに、前のクリーンアップが走る
return
;
};
アーキテクチャの視点:なぜこれが「勝てる」のか
この実装の美しさは、Reactのレンダリングサイクルと非同期処理のライフサイクルを完全に同期させている点にあります。
1. メモリ効率: 不要になった古いPromiseの結果は、JavaScriptエンジンによってガベージコレクションの対象となります。Reactの状態更新(`setData`)が走らないことで、不要な再レンダリングを未然に防ぎます。
2. 不変性の担保: どのリクエストが「最新」の正当な結果であるかを、フラグ(`isCurrent`)という極めて軽量な仕組みで管理します。複雑な状態管理ライブラリを持ち出すまでもなく、ローカルなスコープ内で完結します。
3. パフォーマンス: コンポーネントのアンマウント時にも適切にフラグが折れるため、メモリリークのリスクもゼロです。
さらにその先へ:AbortControllerの活用
もしあなたがモダンな `fetch` APIを使用しているなら、`AbortController` を組み合わせるのが最強の布陣です。フラグで処理を無視するだけでなく、ネットワーク通信そのものをキャンセルすることで、ブラウザのリソース消費を最小化できます。
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== ‘AbortError’) {
console.error(err);
}
});
return () => controller.abort(); // 通信を即時遮断
}, [url]);
—
最後に:現場のエンジニアへ送る言葉
「動くコード」と「壊れないコード」の差は、こうした非同期処理の制御に現れます。レースコンディションは往々にして、開発環境の高速なネットワークでは再現しません。しかし、ユーザーの不安定な回線においては、確実にその牙を剥きます。
`useEffect` のクリーンアップ関数を使いこなすことは、Reactの魔法を解き明かし、その内部エンジンを支配下に置く第一歩です。泥臭いフラグ管理を厭わず、美しいクリーンアップを実装してください。それが、あなたの書くアプリケーションを「信頼できるプロダクト」へと昇華させる唯一の道なのですから。

コメント