Reactのカスタムフックでリクエストのキャンセル処理を追加する方法!初心者向けにやさしく解説
生徒
「先生、ReactでAPIを呼び出すときに、画面を移動するとエラーが出ることがあるんですが、どうすればいいんですか?」
先生
「それはリクエストのキャンセル処理をしていないのが原因かもしれません。画面を切り替えた後もリクエストが動いていると、不要な処理が走ってしまうんです。」
生徒
「なるほど!じゃあ、そのキャンセル処理ってどうやってやるんですか?」
先生
「ReactのカスタムフックでAbortControllerという仕組みを使えば、リクエストを途中で止められますよ。」
1. リクエストのキャンセルとは?
APIリクエストは、一度送るとサーバーからの返事を待つ必要があります。しかし画面を移動したり、同じAPIを短時間に何度も呼ぶと、不要なリクエストが残ってエラーやパフォーマンス低下の原因になります。
このときに使うのが「リクエストのキャンセル処理」です。これは「もう必要ないから、このリクエストは途中で止めていいですよ」とブラウザに伝える仕組みです。
イメージとしては「宅配便を注文したけど、すぐにキャンセルしたいときに配達を止める」ような感じです。
2. カスタムフックにキャンセル処理を追加する
ReactのカスタムフックにAbortControllerを組み込むことで、APIリクエストを安全にキャンセルできます。
import { useState, useEffect } from "react";
function useCancelableApi(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
setLoading(true);
setError(null);
fetch(url, { ...options, signal })
.then((res) => {
if (!res.ok) {
throw new Error("データ取得に失敗しました");
}
return res.json();
})
.then((json) => setData(json))
.catch((err) => {
if (err.name !== "AbortError") {
setError(err.message);
}
})
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
export default useCancelableApi;
ここでは、AbortControllerを作成して、リクエストに信号を渡しています。そしてクリーンアップ処理の中でcontroller.abort()を呼ぶことで、不要になったリクエストをキャンセルしています。
3. 実際に使ってみる
作成したカスタムフックを使って、APIのデータを取得してみましょう。途中で画面を切り替えてもエラーが出なくなります。
import React from "react";
import useCancelableApi from "./useCancelableApi";
function App() {
const { data, loading, error } = useCancelableApi("https://api.example.com/posts");
if (loading) return <p>読み込み中です...</p>;
if (error) return <p>エラーが発生しました: {error}</p>;
return (
<div>
<h1>記事一覧</h1>
<ul>
{data && data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
export default App;
4. リクエストキャンセルを入れるメリット
カスタムフックにリクエストのキャンセル処理を入れると、次のようなメリットがあります。
- 不要なリクエストが残らないのでアプリが軽くなる
- 画面を切り替えてもエラーが出にくくなる
- 複数のAPI呼び出しを安全に扱える
初心者のうちは意識しなくても動きますが、規模の大きなアプリや通信が多いアプリでは必須のテクニックです。
5. よくある使いどころ
リクエストのキャンセル処理は次のような場面でよく使われます。
- 検索フォームで入力するたびにAPIを呼び出すとき(古い検索リクエストをキャンセル)
- 画面遷移したときに不要な通信を止めたいとき
- 複数のタブやボタンで同じデータを取得する処理を使うとき
このように、ユーザーの操作をスムーズにするために、キャンセル処理はとても役立ちます。
まとめ
React開発において、APIとの通信は避けて通れない要素ですが、その裏側にある「非同期処理の制御」は意外と見落とされがちです。今回の記事では、AbortControllerを活用して、カスタムフック内にリクエストのキャンセル機能を組み込む方法について詳しく解説しました。
なぜリクエストのキャンセルが重要なのか
モダンなウェブアプリケーションでは、ユーザーが頻繁にページを遷移したり、検索キーワードを素早く入力したりします。こうしたアクションのたびにネットワークリクエストが発生しますが、レスポンスが返ってくる前に次のアクションが行われた場合、古いリクエストの結果が後から届いてしまい、意図しない画面更新(メモリリークやステートの不整合)を引き起こす可能性があります。
特に「マウントされていないコンポーネントのステートを更新しようとする」エラー(Can't perform a React state update on an unmounted component)を防ぐために、useEffectのクリーンアップ関数でのキャンセル処理は必須の知識と言えます。
AbortControllerの仕組みと実装ポイント
JavaScript標準のAbortControllerは、signalというオブジェクトを通じて通信を制御します。fetch関数のオプションにこのsignalを渡すことで、外部からその通信を「中断」させることができるようになります。
カスタムフック化する際は、以下のポイントを意識しましょう。
- 依存配列の管理:
useEffectの依存配列にURLを含めることで、URLが変わるたびに古いリクエストをキャンセルし、新しいリクエストを開始できます。 - エラーハンドリング: キャンセルされた場合は
AbortErrorが発生するため、これを通常のネットワークエラーと区別して処理する必要があります。 - 再利用性: ロジックをカスタムフックに閉じ込めることで、どのコンポーネントからも簡単に安全な通信を呼び出せるようになります。
さらに応用:複数リクエストの管理
実務では、複数のAPIを同時に叩くシーンも多いでしょう。その場合でも、基本は同じです。各リクエストに対して個別のシグナルを割り当てるか、共通のコントローラーで一括管理するかを設計に合わせて選択します。以下に、より汎用性を高めたTypeScript(TSX)版のカスタムフック例を紹介します。
import { useState, useEffect } from "react";
/**
* 汎用的なデータフェッチ用カスタムフック
* Genericsを使用して、返り値の型を柔軟に定義できます
*/
function useFetchData<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// 1. AbortControllerのインスタンスを作成
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 result = await response.json();
setData(result);
setError(null);
} catch (err: any) {
// キャンセルによるエラーの場合は何もしない
if (err.name === "AbortError") {
console.log("リクエストがキャンセルされました");
} else {
setError(err.message || "エラーが発生しました");
}
} finally {
setLoading(false);
}
};
fetchData();
// 2. クリーンアップ関数でキャンセルを実行
// コンポーネントのアンマウント時やURL変更時に呼ばれる
return () => {
controller.abort();
};
}, [url]);
return { data, loading, error };
}
export default useFetchData;
このコードでは、async/await構文を使用して非同期処理をより読みやすく記述しています。また、AbortErrorを条件分岐で除外することで、ユーザーに不要なエラーメッセージを見せないように工夫しています。
実務でのパフォーマンスへの影響
リクエストを適切にキャンセルすることは、ユーザー体験(UX)の向上に直結します。例えば、低速なネットワーク環境下でユーザーがリンクを連打した場合、キャンセル処理がないとバックグラウンドで大量の通信が滞留し、ブラウザの動作が重くなってしまいます。こうした「見えない部分の配慮」ができるかどうかが、プロのフロントエンドエンジニアへの第一歩です。
また、最近では React Query (TanStack Query) や SWR といった強力なデータフェッチライブラリを使うことも増えていますが、これらのライブラリも内部的には同様のキャンセルロジックをサポートしています。基礎となる AbortController の使い方を理解しておくことで、ライブラリのドキュメントを読み解く力も格段に上がるはずです。
生徒
「先生、ありがとうございました!AbortControllerを使うだけで、あんなに悩んでいた画面遷移時のエラーがスッキリ解決しました。」
先生
「それは良かったです!コードの書き方は少し複雑に見えるかもしれませんが、一度カスタムフックにしてしまえば、あとは使い回すだけですから便利でしょう?」
生徒
「はい。クリーンアップ関数の中で controller.abort() を呼ぶという流れが、Reactのライフサイクルと完璧に噛み合っていて感動しました。これ、検索フォームのインクリメンタルサーチとかにも使えますよね?」
先生
「その通りです!文字を入力するたびにAPIを叩くような場面では、古いキーワードのリクエストをキャンセルしないと、結果がバラバラに表示される『競合状態』が起きやすいんです。まさに現場で必須の技術ですよ。」
生徒
「なるほど。ただ、エラーハンドリングで err.name !== 'AbortError' と書くのを忘れちゃいそうです。これがないと、キャンセルしただけで画面に『エラーが発生しました』って出ちゃいますもんね。」
先生
「鋭いですね。キャンセルは『失敗』ではなく『意図的な中断』なので、ユーザーには知らせないのが一般的です。これからは、API通信を書くときは常にセットで考える癖をつけてみてください。次は、この処理をさらに効率化するために useMemo や useCallback と組み合わせる方法も見ていきましょうか。」
生徒
「ぜひお願いします!もっと安定したReactアプリが作れるようになりたいです!」