【入門編】 非同期処理におけるレースコンディションの回避 – React実践ガイド

こんにちは!Reactの学習、楽しく進められていますか?
画面がサクサク動いて、自分の思い通りに表示が変わっていくのを見ると、本当にワクワクしますよね。

でも、Reactを触り始めて少し経つと、誰もが一度はこんな「不思議な現象」に頭を悩ませます。

「あれ? 検索窓に『abc』って急いで入力したのに、なぜか遅れて『a』の検索結果が表示されて画面が上書きされちゃった……!」

そう、これこそが今回お話しする「レースコンディション(競争状態)」という、非同期処理のちょっと意地悪な罠なんです。

大丈夫ですよ。最初はみんなここでつまずきますし、仕組みさえ分かってしまえば怖くありません。今日は、身近な「お買い物の注文」にたとえながら、この厄介者をスッキリ退治する方法を一緒に見ていきましょう!

—

そもそも「レースコンディション」ってなに?

難しく考える必要はありません。これを身近な例でたとえてみましょう。

あなたはネットショップで、大人気のお洋服の色を「赤」から「やっぱり青!」、そして「やっぱりもう一回赤!」と、すごいスピードで3回連続で注文ボタンを押しました。

スタッフ(サーバー)はそれぞれ注文の控えを受け取り、倉庫へ走り出しました。
1. 「赤の注文書」を受け取ったスタッフA
2. 「青の注文書」を受け取ったスタッフB
3. 「もう一回の赤の注文書」を受け取ったスタッフC

このとき、スタッフB(青)の足がものすごく遅くて、スタッフC(赤)よりもあとに戻ってきたとします。
そうすると、あなたの手元には「一番最後に赤を頼んだはずなのに、なぜか青が届いた!」という現象が起きてしまいますよね。

これがプログラムの世界での「レースコンディション」です。
ユーザーが素早く操作したとき、「先に出発した古いリクエストの返事が、あとから遅れて到着して、新しく到着したはずの結果を上書きしてしまう」というバグが起きるんです。

Reactの `useEffect` でAPIからデータを取るときにも、これとまったく同じことが起こります。

—

クリーンアップ関数という「お片付け」の魔法

Reactの `useEffect` には、「クリーンアップ関数」という、とってもえらいお掃除係の機能が用意されています。

先ほどの注文のたとえで言うなら、「新しい注文書を出したんだから、さっき出した古い注文書の処理はもうキャンセルしてね!」とスタッフに伝える仕組みです。

具体的には、フラグ(目印の変数)を用意して、「コンポーネントが消えたとき」や「次の再描画(再実行)が起きる直前」に、『もうこの古い結果は捨てていいよ!』とフラグを書き換えるというアプローチを取ります。

言葉だけだと少し難しく感じるかもしれないので、実際のコードを見てみましょう!

—

実装コード:フラグ管理で安全な非同期処理を作ろう

検索ワードを入力すると、APIからデータを取得してくるコンポーネントを想像してください。ここに、クリーンアップとフラグ管理を優しく組み込んでみます。

import React, { useState, useEffect } from ‘react’;

// ダミーのAPI通信(実際はここでサーバーからデータを取ってくると思ってください)
const fetchSearchResult = (query) => {
return new Promise((resolve) => {
// 検索ワードごとにわざと通信のスピードを変えています(古い方が遅く返ってくる意地悪な状況)
const delay = query === ‘a’ ? 2000 : 500;
setTimeout(() => {
resolve(`「${query}」の検索結果です!`);
}, delay);
});
};

export default function SearchApp() {
const [query, setQuery] = useState(”);
const [result, setResult] = useState(”);
const [loading, setLoading] = useState(false);

useEffect(() => {
// 1. 何も入力されていないときは何もしない
if (!query) {
setResult(”);
return;
}

// 2. このエフェクト専用の「有効フラグ」を立てる!
let isCurrent = true;

setLoading(true);
console.log(`🚀 「${query}」のリクエスト出発!`);

// 非同期処理のスタート
fetchSearchResult(query)
.then((data) => {
// 3. ここが一番のポイント!
// 「もし、このリクエストが最新のものなら(フラグが折れていなければ)」画面を更新する
if (isCurrent) {
setResult(data);
setLoading(false);
console.log(`✅ 「${query}」の結果を採用しました!`);
} else {
console.log(`🗑️ 「${query}」はもう古いので捨てました。`);
}
});

// 4. クリーンアップ関数(お掃除係)
// 次の useEffect が走る直前、またはこの画面が消えるときに呼ばれます
return () => {
// 「私、もう古くなっちゃったから、次の人にバトンタッチするね!」とフラグを折る
isCurrent = false;
};
}, [query]); // query(検索ワード)が変わるたびにこの処理が生まれ変わります

return (

お買い物・検索のレースコンディション対策デモ

setQuery(e.target.value)}
placeholder=”文字を入力してね(例: a, ab…)”
style={{ padding: ‘8px’, fontSize: ’16px’, width: ‘300px’ }}
/>

{loading &&

読み込み中…

}

{result}

);
}

コードの心臓部を優しく解説

1. `let isCurrent = true;` ってなに?
useEffectが実行されるたびに、その時だけの「私は最新です!」という証明書(フラグ)を作ります。
2. `return () => { isCurrent = false; };` の魔法
ユーザーが文字を追加して `query` が変わると、Reactは「新しいuseEffectを実行する前に、古いuseEffectのお片付け(クリーンアップ)をしよう」と動きます。ここで `isCurrent = false` にすることで、「さっきまでの私はもう古いよ」と宣言するわけです。
3. `if (isCurrent)` でのガード
通信が終わって結果が返ってきたとき、「おや、私の証明書はまだ有効かな?」と確認します。もしユーザーがすでに次の文字を入力していて自分が古くなっていれば、`setResult` を実行せず、結果をそっとゴミ箱に捨てます。これで古いデータに画面を上書きされるのを完璧に防げます!

—

チーム開発や実務におけるワンポイント・プロの視点

今回の「フラグによる管理」は、クリーンアップの仕組みを直感的に理解する上で最高の教材です。

ただ、実務の現場では、もっとスマートにこれを解決する方法もよく使われます。
例えば、世界中の多くの開発者が愛用しているデータフェッチライブラリ(TanStack Query や SWR など)を使うと、今回のようなレースコンディションの回避やキャッシュ管理を、ライブラリ側が裏側でいい感じに全部やってくれます。

「うわ、ライブラリって便利だな!」と思う前に、まずは今日学んだような「Reactのライフサイクルと、クリーンアップ関数がどうやって古いものを掃除しているのか」という原始的な仕組みを頭の片隅に置いておいてください。この基礎体力があるだけで、複雑なバグに遭遇したときも「あ、これレースコンディションだな」と秒速で原因に気づけるようになりますよ。

—

まとめ

  • レースコンディションとは?:古いリクエストが遅れて返ってきて、新しい画面を上書きしちゃう意地悪な現象。
  • 解決の鍵:`useEffect` のクリーンアップ関数を使って、古い処理に「もう用済みだよ(`isCurrent = false`)」と教えてあげること。
  • 焦らなくて大丈夫:最初は「難しそう……」と感じるのが普通です。まずは上のコードをご自身の環境に貼り付けて、コンソールのログを眺めながら「お、ちゃんと古いのが捨てられてる!」という体験を楽しんでみてくださいね。

あなたのReact開発の旅が、少しでも楽しく、スムーズになりますように。応援しています!

コメント

タイトルとURLをコピーしました