【入門編】 非同期イベントハンドラ内での状態更新 – React実践ガイド

こんにちは!Reactの学習、楽しく進められていますか?
画面がスイスイ動く魔法のようなアプリが作れる一方で、触れば触るほど「あれ?なんだか画面の表示が古いまま変わらないぞ……?」とか、「コンソールに変なエラーが出て赤くなっちゃった!」という壁にぶつかったりしていませんか?

大丈夫ですよ。そのモヤモヤ、世界中のすべてのReactエンジニアが最初に必ず通る「登竜門」なんです。今日はその中でも、特に初心者のハートをバキバキに折りやすい「非同期処理の中での状態(state)更新」について、一緒に優しく紐解いていきましょう!

—

1. なぜ「非同期」だと状態がおかしくなるの?(お買い物のレシートに例えてみよう)

まずは、Reactの `useState` の正体を、身近な例えで考えてみます。

想像してください。あなたは今、大きなお祭りの屋台で、大人気の「焼きそば」を買いに行きました。
レジの店員さん(React)は、あなたの注文を聞くたびに、メモ帳(State)に「現在の売り上げ枚数:5枚」と書き留めています。

ここで、こんなシチュエーションを想像してください。
1. あなたが「焼きそばを1つください!」と注文します。
2. 店員さんは「わかりましたー、今から焼きそばを作ってきますね(API通信や `setTimeout` などの非同期処理のスタート)」と、奥の調理場へ引っ込みます。
3. その間に、別のお客さんが来て「焼きそば2つ!」と注文しました。店員さんのメモ帳の数字は、あっという間に「7枚」に書き換わります。
4. さて、しばらくして、最初に注文を作っていた店員さんが戻ってきて、こう言いました。
「ええっと、私が生徒さんから注文を受けたときのメモだと、売上は『5枚』だったから……そこに『1』を足して、『6』に書き換えよっと!」

……おっと!これ、大変なことになっていませんか?
別のお客さんの注文(2枚分)が、きれいさっぱり消えて(上書きされて)しまいましたよね!

これが、Reactの非同期処理(`async/await` や `Promise`、あるいは `setTimeout` など)の中で、古い状態の値をそのまま使ってしまったときに起こる悲劇の正体です。

—

2. 関数型アップデート(前の状態を安全に引き継ぐ魔法)で解決しよう!

Reactで「今の状態に何かを足したり書き換えたりする」とき、そのままの変数を使っていると、先ほどのレジの店員さんのようなミスが起きます。

そこで登場するのが、「関数型アップデート」という、実務でもめちゃくちゃお世話になる書き方です。

言葉を聞くと難しそうですが、要するにとびきりシンプルなルールです。
`setCount(count + 1)` と書く代わりに、「セッター関数の中に、『古い状態を受け取って、新しい状態を返すアロー関数』をポイッと放り込む」だけ!

実際のコードで見てみましょう。ボタンを押すと、2秒後にカウントが「+1」される、ちょっと意地悪な非同期のカウンターを作ってみます。

import React, { useState } from ‘react’;

export default function AsyncCounter() {
const [count, setCount] = useState(0);

// 2秒待ってからカウントを増やすお仕事(非同期処理)
const handleAsyncClick = () => {
// 2秒という長い時間の間に、もし他のボタン操作があっても大丈夫なようにします
setTimeout(() => {
// ❌ やっちゃいけない書き方(古い count の値を直接使っちゃう)
// setCount(count + 1);

// ⭕️ おすすめの書き方:関数型アップデート!
// 「現在の最新の状態(prevCount)」を安全に受け取って計算します
setCount((prevCount) => {
console.log(‘今の最新のカウントはこれだよ:’, prevCount);
return prevCount + 1;
});
}, 2000);
};

return (

);
}

この書き方にしておけば、たとえ2秒のタイムラグの間に何が起きようとも、Reactが「その瞬間の一番新しい状態(`prevCount`)」をちゃんと手渡してくれるので、計算が狂わなくなります。安心ですね!

—

3. もう一つの大敵:コンポーネントが消えた後の「メモリリーク警告」

非同期処理を扱うときにもう一つ、初心者の心をへし折る厄介なエラーがあります。
コンソールを覗いたときに、こんな赤文字を見たことはありませんか?

> Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak.

日本語にざっくり訳すと、「もう画面から消いなくなった(アンマウントされた)コンポーネントに対して、状態を更新しようとしないでよ!メモリが無駄になっちゃうよ!」という、Reactからの怒られ通知です。

どんなときに起きるの?

こんなシチュエーションを想像してください。
1. ユーザーが「プロフィール詳細ページ」を開きました。
2. ページを開いた瞬間、「サーバーからユーザーデータを取ってくるぞー(API通信のスタート)」と非同期処理が動き出します。
3. でも、ユーザーが「あ、間違えた!」と思って、データが返ってくる前の0.5秒足らずで、すぐに「ホーム画面」に戻ってしまいました。
4. 画面上からプロフィールページは消え去っているのに、裏側でこっそり走っていた非同期処理が「データ取れたよー!さあ状態を更新するぞ!」と `setUserData(…)` を実行します。
5. 「いないはずの幽霊に向かって話しかけてるよ!」ということで、Reactがエラーを投げるわけです。

—

4. クリーンアップ関数(またはフラグ)で幽霊の暴走を防ぐ!

この「画面から消えたあとの状態更新」を防ぐためには、「今、このコンポーネントはちゃんと画面に存在しているか?」というガードマンを置くのがプロの技です。

一番王道なのは、`useRef` や、`useEffect` のクリーンアップ機能を使う方法ですが、今回はイベントハンドラやタイマー、非同期処理のキャンセルでよく使われる「マウント状態を管理するフラグ(お守り)」のテクニックをご紹介しますね。

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

// ダミーの非同期データ取得コンポーネント
function UserProfile() {
const [data, setData] = useState(‘読み込み中…’);

useEffect(() => {
// 1. 「今、このコンポーネントは画面にいるよ」というフラグを用意
let isMounted = true;

// 非同期でデータを取得するシミュレーション(3秒かかるAPIなど)
const fetchUserData = async () => {
try {
// 3秒待つ
await new Promise((resolve) => setTimeout(resolve, 3000));

// 2. 3秒経ってデータが返ってきたとき、まだ画面にいるかチェック!
if (isMounted) {
setData(‘ユーザー情報の取得に成功しました!’);
}
} catch (error) {
if (isMounted) {
setData(‘エラーが発生しました。’);
}
}
};

fetchUserData();

// 3. 画面から消える(アンマウントされる)瞬間にこの関数が呼ばれる
return () => {
isMounted = false; // 「もう画面からいなくなったよ!」とフラグを倒す
console.log(‘コンポーネントが消えたので、非同期の結果反映はキャンセルします!’);
};
}, []);

return (

プロフィールカード

{data}

※「3秒経つ前」に別のページに移動(または親コンポーネントで非表示に)してみてください。
エラーが出ずに安全に処理がスルーされます!

);
}

export default function App() {
const [showProfile, setShowProfile] = useState(true);

return (

{showProfile && }

);
}

このコードでは、`useEffect` の中で `isMounted` という旗(フラグ)を持たせています。
もしユーザーが3秒待たずに「非表示にする」ボタンを押してコンポーネントを消しても、`return` の中で `isMounted = false` に書き換えられるため、後からデータが返ってきても `setData` が実行されず、エラーを華麗に回避できるという仕組みです。

実務の現場でも、API通信のライブラリ(AxiosのCancelTokenや、AbortControllerなど)を使って、不要になった通信そのものをバッサリ中断するアプローチと一緒に、よく使われるお作法となっています。

—

まとめ:一歩ずつ、確実にマスターしていこう!

お疲れ様でした!今日学んだ大切なポイントをギュッと凝縮して振り返ってみましょう。

1. 非同期の中での状態更新は、古い値に要注意!

  • レジの例えのように、タイムラグの間に状態が変わってしまうリスクがあります。

2. 関数型アップデート(`setCount(prev => prev + 1)`)を使おう!

  • 常に最新の安全な状態を引き継ぐための、Reactの心強い相棒です。

3. 画面から消えた後の更新には「フラグ」でお守りをしよう!

  • 幽霊コンポーネントへの指示出しを防いで、コンソールを綺麗なエラーフリーに保ちましょう。

最初は「うわ、考えることがいっぱいあるな……」と感じるかもしれませんが、何度かコードを書いてエラーに遭遇していくうちに、体の一部のように自然と手が動くようになっていきます。

もしまたコードが思い通りに動かなくなったら、「あ、今の私はレジの店員さん状態になってないかな?」と、今日の例えをふと思い出してみてくださいね。
あなたのReactライフを、心から応援しています!一緒に楽しくコードを書いていきましょう!

コメント

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