お疲れ。少し手を止めて、コーヒーでも飲みながら聞いてくれ。
君もそろそろ「Reactの基本は一通りマスターした、実務の機能開発も任せてくれ」というフェーズだと思う。だがな、中級の壁として必ず立ちはだかるのが「非同期処理と状態管理の絶妙なズレ」なんだよ。
コードレビューをしていて、一番頭を抱えたくなるのがここだ。`async/await` を使ったボタンのクリックハンドラ。APIからデータを取ってきて、ローディングを戻して……一見、綺麗に書けているように見えるそのコード、本当に安全か?
ユーザーがせっかちに画面をパッパと切り替えたとき、ブラウザのコンソールに「Can’t perform a React state update on an unmounted component…」なんていう、あの悪名高い赤字エラーが出ていないか?
今日は、現場で生きるプロとして、この「非同期イベントハンドラ内での状態更新の罠」と、その泥臭くて確実な対策を叩き込んでやる。心して聞いてくれ。
—
1. なぜ非同期処理の中で状態(State)更新がバグるのか?
まずは敵を知ることから始めよう。Reactの `useState` は、ただの変数じゃない。Reactのレンダリングライフサイクルと密に結合された「トリガー」だ。
よくあるのが、こんなコードだ。
// ⚠️ ありがちな危ういコード
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const handleClick = async () => {
setLoading(true);
// 重いAPIリクエストを待つ(非同期)
const result = await fetchSomething();
// この瞬間、コンポーネントがすでに画面から消えているかもしれない!
setData(result);
setLoading(false);
};
何が問題か分かるか?
`await` で待っている間に、ユーザーが別のページに遷移したり、親コンポーネントの条件分岐によってこのコンポーネントがアンマウント(DOMから削除)されたとする。
JavaScriptの実行コンテキストはそのまま非同期処理の続き(`await` の下)を実行し、存在しないコンポーネントの `setData` や `setLoading` を呼び出そうとする。Reactはここで「おい、もういない奴のステートを更新しようとしてるぞ!」と警告を鳴らすわけだ。
React 18以降、このエラーは自動的なメモリリーク検出の観点から挙動が変わった部分もあるが、「アンマウントされたコンポーネントのステートを書き換えることによる無駄な処理」や「予期せぬバグ」が消えたわけではない。 ここを舐めてると、実務のプロダクション環境で痛い目をみる。
—
2. 実務で使えるベストプラクティス:安全な状態更新の二大アプローチ
じゃあ、どうするか? 現場で私たちが使っている実践的な防衛策は主に2つある。
1. `useRef` を使ったマウント状態の追跡(フラグ管理)
2. AbortController による通信自体のキャンセル(根本治療)
この2つを組み合わせるのが、プロのフロントエンドエンジニアの仕事というものだ。それぞれコードを見ていこう。
アプローチA: `useRef` でマウント状態を監視する(一番手軽で確実な保険)
コンポーネントが生きているかどうかを `useRef` で追跡する。`useRef` は値を書き換えても再レンダリングを引き起こさないため、こういう「裏でのフラグ管理」にはうってつけの相棒だ。
import React, { useState, useEffect, useRef } from ‘react’;
export const SafeAsyncButton = () => {
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState(”);
// 1. マウント状態を保持するRefを作成
const isMountedRef = useRef(true);
useEffect(() => {
// マウント時にtrueにする
isMountedRef.current = true;
// アンマウント時(クリーンアップ関数)にfalseにする
return () => {
isMountedRef.current = false;
};
}, []);
const handleAction = async () => {
setLoading(true);
setMessage(”);
try {
// 模擬的な重い非同期処理(2秒かかるAPI叩き)
await new Promise((resolve) => setTimeout(resolve, 2000));
// 2. 処理が再開した時点で、まだコンポーネントが生きてるかチェック!
if (!isMountedRef.current) {
console.log(‘すでにアンマウントされているため、状態更新をスキップします。’);
return;
}
setMessage(‘データの取得に成功しました!’);
} catch (error) {
if (!isMountedRef.current) return;
setMessage(‘エラーが発生しました。’);
} finally {
// 3. ここでも生存確認を忘れない
if (isMountedRef.current) {
setLoading(false);
}
}
};
return (
{message}
);
};
どうだ? これだけでもう、「消えた幽霊コンポーネントのステートを叩く」という恐怖から解放される。コピペしてすぐにでもチームのプロジェクトに導入できるはずだ。
—
アプローチB: `AbortController` で通信そのものをキャンセルする(さらにワンランク上のアプローチ)
ステートの更新を防ぐだけじゃなく、そもそも「不要になった通信をブラウザ側でバッサリ切り捨てる」のが、ネットワーク帯域やサーバー負荷を考慮したプロの技だ。
ここで登場するのが、Web標準APIの `AbortController` だ。
import React, { useState, useEffect } from ‘react’;
export const AbortableFetchComponent = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
// 1. Controllerのインスタンスを生成
const controller = new AbortController();
const { signal } = controller;
const fetchData = async () => {
setLoading(true);
try {
// fetchにsignalを渡すことで、キャンセルが可能になる
const response = await fetch(‘https://api.example.com/data’, { signal });
const json = await response.json();
setData(json);
} catch (error: any) {
// キャンセルされた場合のエラーは無視する
if (error.name === ‘AbortError’) {
console.log(‘通信は正常にキャンセルされました’);
return;
}
console.error(‘その他のエラー:’, error);
} finally {
setLoading(false);
}
};
fetchData();
// 2. コンポーネントが破棄される瞬間に通信をabort(中断)する
return () => {
controller.abort();
};
}, []);
return (
ローディング中…
:
{JSON.stringify(data, null, 2)}
}
);
};
このアプローチの何が素晴らしいって、ユーザーがページを離脱した瞬間に、裏で走っていた無駄な通信ネットワークがパチンと切断されるところだ。モバイル環境や、通信制限のあるユーザーに対して極めて優しい、シニアらしい気配りの効いたコードと言える。
—
3. チーフアーキテクトからの現場の心得
最後に、チームで開発を進める上での心構えを一つ。
非同期処理とReactの状態管理は、一見するとシンプルに見えて、エッジケース(例外的なユーザーの動き)を踏むと一瞬でボロが出る。だからこそ、コードレビューのときは以下の視点を持つように意識してほしい。
- 「この `await` の後、このコンポーネントは確実に存在しているか?」
- 「ユーザーが連打したとき、古いリクエストの結果が新しい結果を上書きする(競合状態:Race Condition)リスクはないか?」(※これについてはまた別の機会に話そう。カスタムフックで `useCallback` や `useRef` を駆使する領域だ)
基礎の積み重ねが、プロダクトの堅牢性を作る。
動くだけのコードは素人にでも書ける。だが、「保守性が高く、どんな荒い操作をされても壊れない美しいコード」を書くのが、我々プロのフロントエンドエンジニアの仕事だ。
よし、今日の知見はここまでだ。各自のプロジェクトコードに戻って、危なっかしい `async/await` がないか点検してみるといい。何か躓いたら、いつでも声をかけてくれ。

コメント