ReactのuseEffectで非同期処理をキャンセルする方法を初心者向けに解説
生徒
「先生、Reactでデータを取ってくる処理を書いたんですが、画面を閉じたあとにも動き続けてしまいました。どうすれば止められるんですか?」
先生
「それは非同期処理をキャンセルしていないからですね。ReactではuseEffectのクリーンアップを使ってキャンセルする方法があります。」
生徒
「キャンセルってどういう意味ですか?」
先生
「たとえるなら、宅配を頼んだ後に外出して受け取れないとわかったらキャンセルしますよね。プログラムでも、もう必要ない処理は途中で止めるのが大切なんです。」
1. 非同期処理とキャンセルの必要性
非同期処理とは、時間がかかる処理を待たずに進める仕組みです。例えば、サーバーからデータを取得するfetch関数などが代表例です。しかし、ユーザーが画面を移動したあともその処理が続いてしまうと、不要なエラーやメモリの無駄遣いが発生します。
そのため、ReactではuseEffectのクリーンアップ関数を使って、処理を途中でキャンセルできるようにします。
2. AbortControllerを使ったキャンセル
AbortControllerというブラウザ標準の機能を使うと、fetchの通信を途中で止められます。ReactのuseEffectと組み合わせると、安全にキャンセル処理を書けます。
import React, { useEffect, useState } from "react";
function App() {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
fetch("https://jsonplaceholder.typicode.com/posts/1", { signal })
.then((res) => res.json())
.then((result) => setData(result))
.catch((error) => {
if (error.name === "AbortError") {
console.log("通信をキャンセルしました");
} else {
console.error(error);
}
});
return () => controller.abort();
}, []);
return (
<div>
<h1>データ表示</h1>
{data ? <p>{data.title}</p> : <p>読み込み中...</p>}
</div>
);
}
export default App;
3. フラグを使ったシンプルなキャンセル方法
AbortControllerを使わなくても、フラグを使って「この処理はもう不要」と判断する方法もあります。これは古くから使われているシンプルなやり方です。
import React, { useEffect, useState } from "react";
function App() {
const [data, setData] = useState(null);
useEffect(() => {
let isActive = true;
fetch("https://jsonplaceholder.typicode.com/posts/2")
.then((res) => res.json())
.then((result) => {
if (isActive) {
setData(result);
}
});
return () => {
isActive = false;
};
}, []);
return (
<div>
<h1>記事データ</h1>
{data ? <p>{data.title}</p> : <p>読み込み中...</p>}
</div>
);
}
export default App;
4. 実際の開発での利用場面
非同期処理のキャンセルは次のような場面で役立ちます。
- 検索フォームで入力が変わるたびにデータを取得する場合
- ページを移動するときに不要な通信を止めたい場合
- 大量のデータを扱うアプリで無駄な処理を避けたい場合
これらを正しく扱うと、アプリは無駄な負担が減り、ユーザー体験が良くなります。
5. 初心者が注意すべきポイント
キャンセル処理を忘れると、Reactで「アンマウント後に状態を更新しようとしました」という警告が出ることがあります。これは、画面が閉じたあとにもsetStateが呼ばれているからです。
このエラーを防ぐには、必ずクリーンアップ処理を使うこと、そして非同期処理を安全に止める習慣をつけることが大切です。
まとめ
React開発において、useEffect内での非同期処理の制御は、アプリケーションの品質を左右する非常に重要な要素です。今回の記事では、データの取得中にコンポーネントが画面から消えてしまった(アンマウントされた)際に、どのようにしてその処理を中断させるかという「キャンセル処理」に焦点を当てて解説してきました。
フロントエンド開発、特にシングルページアプリケーション(SPA)では、ユーザーは頻繁にページやタブを切り替えます。そのたびにサーバーへのリクエストが走り、もしキャンセル処理が行われていないと、もはや存在しないコンポーネントに対して「データを画面に反映させて!」という命令が飛び続けてしまいます。これが、よく見かける「メモリリーク」や「不要なエラーログ」の原因となります。
非同期処理キャンセルの2大手法の比較
本記事で紹介した2つの手法は、どちらも現場で頻繁に使われるものです。それぞれの特徴を整理してみましょう。
| 手法 | メリット | デメリット |
|---|---|---|
| AbortController | ネットワーク通信自体を即座に中断できるため、通信リソースを節約できる。 | catchブロックでのエラーハンドリング(AbortErrorの判定)が必要。 |
| フラグ管理(isActive) | ロジックが単純で分かりやすく、fetch以外の非同期処理にも応用しやすい。 |
通信自体は裏側で最後まで続いてしまうため、リソース消費の改善にはならない。 |
実践的な実装例:カスタムフックへの応用
実際のプロジェクトでは、これら一連のキャンセル処理を毎回書くのは大変です。そのため、以下のようにカスタムフックとして共通化することが推奨されます。ここでは、より実戦に近いTypeScript(React TSX)での記述例を見てみましょう。
import { useState, useEffect } from "react";
// カスタムフック:データを安全に取得する
function useFetchData(url: string) {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error("ネットワーク応答が正常ではありません");
}
const json = await response.json();
setData(json);
setError(null);
} catch (err: any) {
if (err.name === "AbortError") {
console.log("フェッチがキャンセルされました");
} else {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchData();
// クリーンアップ関数で通信を中断
return () => {
controller.abort();
};
}, [url]);
return { data, loading, error };
}
export default useFetchData;
パフォーマンスとユーザー体験の向上に向けて
Reactのフック(Hooks)を使いこなすということは、単に動くコードを書くだけでなく、アプリケーションが「背負っている負荷」をいかに軽くするかを考えることでもあります。
特に、検索エンジンのSEO対策が必要な公開サイトや、モバイル端末で利用されるWebアプリでは、無駄なデータ転送量を削ることが読み込み速度の向上に直結します。useEffectのクリーンアップは、そのための第一歩です。
また、最新のReact開発では「React Query」や「SWR」といった、強力なデータ取得ライブラリが使われることも増えています。これらのライブラリは、内部で今回解説したようなキャンセル処理を自動的に行ってくれます。しかし、その根本的な仕組みを知っているのと知らないのとでは、トラブルが起きた際の原因特定能力に大きな差が出ます。
基本をしっかりと押さえた上で、より高度なライブラリやフレームワーク(Next.jsなど)へとステップアップしていきましょう。コードの一行一行がユーザーのストレスを減らすことに繋がっていると意識すると、プログラミングがもっと楽しくなるはずです。
生徒
「先生、ありがとうございました!useEffectの中でreturnを使って関数を返すだけで、あんなに大事なキャンセル処理ができるなんて驚きでした。」
先生
「そうですね。クリーンアップ関数は『後片付け』の役割を担っています。これを使わないと、プログラムが散らかり放題になって、最終的にアプリが重くなったり動かなくなったりするんですよ。」
生徒
「さっきのAbortControllerの話ですが、エラーハンドリングの中でAbortErrorかどうかをチェックするのはなぜですか?」
先生
「良い質問ですね!キャンセルというのは、通信エラーのような『失敗』ではなく、意図的に『止めた』だけなんです。だから、それを普通のエラーとして画面に出してしまうとユーザーが混乱してしまいます。それを防ぐために区別しているんですよ。」
生徒
「なるほど。お掃除と同じで、次に使う時に困らないように、終わった後のことを考えるのがプロの書き方なんですね。Next.jsとかを使っていても、この考え方は同じですか?」
先生
「その通りです。フレームワークが変わっても、非同期処理のライフサイクルを管理するという原則は変わりません。まずはこの基本を自分のものにして、どんな環境でも安定したアプリを作れるようになりましょう!」
生徒
「はい!これからは『やりっぱなし』にしないコードを心がけます。次はもっと複雑なAPI連携にも挑戦してみたいです!」