こんにちは!Reactの世界へようこそ!
Reactを触り始めて、「画面が動いた!楽しい!」と思えるようになってきた頃に、ひっそりと忍び寄ってくるのが「非同期処理のタイミング問題」です。
検索窓に文字をサクサク入力しているときや、ボタンを連打したときに、「あれ? 最後に押したデータの画面になっていない…?」という不思議な現象に出会ったことはありませんか?
「自分のコードのどこが間違っているんだろう…」と不安にならなくても大丈夫ですよ!これは「レースコンディション(競合状態)」と呼ばれる現象で、React開発者が必ず一度は通る「お約束の壁」なんです。
今回は、この問題を「たった1行のフラグ」と「クリーンアップ関数」でスマートに解決するプロの技を、超やさしく解説しますね。一緒にゆっくり紐解いていきましょう!
—
そもそも「レースコンディション」ってなに?
難しい専門用語に聞こえますが、身近な例えで考えると一発で分かります!
レストランの注文をイメージしてみてください。
1. あなたは店員さんに「Aセット(うどん)」を注文しました。
2. 店員Aさんが厨房へ走り出します。
3. でもすぐに気が変わって、別の店員Bさんに「やっぱりBセット(ラーメン)で!」と注文し直しました。
4. 店員Bさんは走ってすぐ「Bセット(ラーメン)」を持ってきてくれました。あなたは美味しく食べ始めます。
5. …するとそこへ、遅れて厨房から戻ってきた店員Aさんが「お待たせしました!Aセット(うどん)です!」と、ラーメンの上にうどんを上書きで置いていってしまいました。
「いやいや、もうラーメン食べてるよ!注文変えたでしょ!」って思いますよね(笑)。
これがWebの世界でも起きているんです。
ネットワークの混雑具合によって、「古いリクエスト(うどん)」が「新しいリクエスト(ラーメン)」よりも遅れて届いてしまい、画面を古い情報で上書きしてしまう。 これが「レースコンディション」の正体です。
—
なぜReactの `useEffect` でこれが起きるの?
Reactの `useEffect` は、依存配列(`[ ]` の中身)が変わるたびに実行されますよね。
例えば、ユーザーのIDが変わったらデータを読み込む処理を書いたとします。
// ⚠️ レースコンディションが起きやすいコードの例
useEffect(() => {
fetchUserData(userId).then(data => {
setUser(data); // 遅れて届いた古いデータが、ここで画面を上書きしちゃう!
});
}, [userId]);
ユーザーが `userId` を「1」→「2」→「3」と素早く切り替えたとき、バックグラウンドでは3つの通信が同時に走ります。
もし「1」の通信がすごく重くて、最後に返ってきてしまったら…?
画面には「3」のユーザーを表示したいのに、最後に届いた「1」のユーザー情報が表示されてしまうのです。
—
解決の切り札!「クリーンアップ関数」と「キャンセル用フラグ」
これを防ぐための超シンプルなベストプラクティスが、「クリーンアップ関数の中でフラグを倒す」というテクニックです。
先ほどのレストランの例で言うなら、「注文を変更した瞬間に、前の店員さんに『さっきの注文はキャンセルで!無視してね!』とメモを残す」というイメージです。
仕組みはとってもシンプル!
1. `useEffect` の中で `let isCancelled = false;` という変数を用意する。
2. データが届いたとき、`if (!isCancelled)`(キャンセルされていなければ)のときだけ画面を更新する。
3. `useEffect` のクリーンアップ関数(`return () => { … }`)で `isCancelled = true;` に変更する。
Reactは次のエフェクトを実行する直前(またはコンポーネントが消える時)に、必ず前のエフェクトの「クリーンアップ関数」を実行してくれるという性質を持っています。
これを利用すれば、新しいリクエストが始まった瞬間に、古いリクエストの結果を「無視する」ことができるんです!
—
実際にコードを書いてみよう!
それでは、そのままエディタに貼り付けて試せるサンプルコードを見てみましょう。
今回は「ユーザーIDを切り替えて、プロフィールを取得するコンポーネント」を作りました。
import React, { useState, useEffect } from ‘react’;
function UserProfile({ userId }) {
// 取得したユーザー情報を保持するステート
const [user, setUser] = useState(null);
// 読み込み中かどうかを表すステート
const [loading, setLoading] = useState(false);
useEffect(() => {
// 1. この処理が「有効かどうか」を表すフラグを作成(初期値は false)
let isCancelled = false;
async function fetchUserData() {
setLoading(true);
setUser(null); // 前のデータをリセット
try {
// 擬似的なAPIリクエスト(通信に時間がかかると仮定)
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
const data = await response.json();
// 2. もしクリーンアップ関数が実行されて「isCancelled」が true になっていたら、
// 遅れて届いたデータなので無視(ステート更新しない)します!
if (!isCancelled) {
setUser(data);
setLoading(false);
} else {
console.log(`ユーザーID: ${userId} のデータは古いため破棄されました。`);
}
} catch (error) {
if (!isCancelled) {
console.error(“データの取得に失敗しました”, error);
setLoading(false);
}
}
}
fetchUserData();
// 3. クリーンアップ関数を返します
// userId が変わった瞬間、または画面から消える直前に React がこれを呼び出します!
return () => {
isCancelled = true; // フラグを true にして、前の通信結果を無効化!
};
}, [userId]); // userId が変化するたびに useEffect が再実行されます
return (
ユーザープロフィール(ID: {userId})
{loading &&
データを読み込み中…
}
{user && !loading && (
名前: {user.name}
メール: {user.email}
会社名: {user.company?.name}
)}
);
}
export default UserProfile;
—
ここが運命の分かれ道!コードのポイント解説
一番大切なのは、以下の流れです。
1. ユーザーが `userId = 1` を選択 ➔ `useEffect` が実行され、`isCancelled`(1専用)が `false` で作られる。
2. すぐにユーザーが `userId = 2` に切り替えた!
3. Reactが「あ、`userId` が変わったから、まずは前回のクリーンアップを実行しよう!」と動く。
4. `userId = 1` の時に作ったクリーンアップ関数が走り、`isCancelled`(1専用)が `true` に上書きされる。
5. その後、遅れて `userId = 1` のレスポンスが届く。
6. `if (!isCancelled)` のチェックで引っかかる(`true` になっているので)。
7. `setUser(data)` が実行されない! (=画面が古いデータで狂わない!)
通信自体を途中で物理的にキャンセルしているわけではありませんが、「遅れて届いた返事は無視して捨てる」 というアプローチによって、画面の表示順序の崩れを100%防ぐことができます。
—
まとめ:もう非同期のバグは怖くない!
最後に、今回のポイントを優しく復習しておきましょう。
- レースコンディションは、非同期処理の到着順序が入れ替わることで起きる画面のチラつきや上書きバグ。
- 解決のカギは 「`useEffect` のクリーンアップ関数」。
- `let isCancelled = false;` を用意して、クリーンアップで `true` に変えるだけ!
- たったこれだけで、最後にユーザーが求めた正しいデータだけが画面に表示される。
このパターンは、実務のフロントエンド開発でも毎日のように使われている非常に強力で信頼性の高いテクニックです。
最初は「クリーンアップ関数がいつ動くのか」ピンとこないかもしれませんが、何度もコードを書いていくうちに「あ、ここでお掃除(クリーンアップ)してくれてるんだな」と実感できるようになりますよ。
焦らず、一歩ずつ進んでいきましょうね。あなたのReactライフがもっと楽しいものになりますように!

コメント