🐶

API連打してない?Reactでありがちなリクエストキャンセル忘れ

に公開

対象読者

Reactで以下のような、Fetch on Renderパターンを書いたことがある人向けの記事です。

useEffect(() => {
  fetch(URL).then((res) => {
    setData(res);
  });
}, []);

「よく書くし、特に問題も起きてないけど…」というあなた。それ、メモリリークの原因になってるかもしれません。

🐶 何が問題なのか?

このコード、コンポーネントがアンマウントされてもリクエストがキャンセルされないという問題があります。

クリックで可愛いわんちゃんが表示されるだけのサンプルを通して、問題点を見ていきましょう。

次の操作をしてください

  1. Chrome DevToolsのNetworkタブを開く
  2. 「切り替え」ボタンを何度か連打する

すると、連打した回数だけAPIがリクエストされているのがわかります。
マウント時に、APIリクエストを呼び出す実装をしているので当然です。

しかし、切り替えたことで非表示になったコンポーネントのリクエスト本来不要のはずです。
これがキャンセルされず通信が続いてしまう。これは無駄ですし、次のようなデメリットがあります。

リクエストがキャンセルされないことによる課題

  • 不要な通信量の増加
  • サーバーリソースの無駄
  • アプリケーションのパフォーマンス低下
  • 結果的にメモリリークやパフォーマンス低下の原因になる

ではどう対処すればいいか、見ていきましょう。

リクエストをキャンセルしよう

データフェッチにはさまざまな方法があるので、方法別にキャンセルする方法を解説していきます。

fetch APIの場合

AbortControllerを利用することで、キャンセルすることができます。

useEffect(() => {
  const controller = new AbortController();
  const signal = controller.signal;

  fetch(URL, { signal })
    .then((res) => res.json())
    .then((res) => setData(res));

  return () => controller.abort(); // アンマウント時にキャンセル
}, []);

修正を反映したサンプルをでも確認することができます。

上記と同じ検証手順で、リクエストがちゃんとキャンセルされていることを確認できます。

https://developer.mozilla.org/ja/docs/Web/API/AbortController

Axiosの場合

Axiosでも同じように AbortController を使えます(v0.22以降)。

useEffect(() => {
  const controller = new AbortController();
  const signal = controller.signal;

  axios
    .get(URL, { signal })
    .then((res) => setData(res.data.message));

  return () => controller.abort();
}, []);

https://axios-http.com/ja/docs/cancellation

TanStack Queryの場合

TanStack Queryはフェッチライブラリではないので、使うfetch関数側でキャンセルに対応する必要があります。

例えば fetch を使っている場合は、以下のように signalqueryFn の引数から取得して利用します。

const { data } = useQuery({
  queryKey: ["dogs"],
  queryFn: async ({ signal }) => {
    const response = await fetch(URL, { signal });
    const data = await response.json();
    return data.message;
  },
});

https://tanstack.com/query/latest/docs/framework/react/guides/query-cancellation

SWRの場合

SWRは公式からキャンセルの仕組みが提供されていません。
ただし、コミュニティではキャンセルの議論がされており、ミドルウェアの活用で対応できる可能性があるようです。

ちなみに、SWRには以下のような仕様もあるため、キャンセルがなくても実害が少ない場合もあります。

  • 同じキーのリクエストが進行中なら、重複リクエストは発生しない
  • 後から来たレスポンスで上書きしないよう制御されている

このような仕様があるのため、公式からはキャンセルの仕組みが提供されていないのかもしれませんね。

おわりに

Reactでは、今回のようなデータ取得のキャンセル忘れだけでなく、

  • setIntervalの解除忘れ
  • addEventListenerの除去漏れ

など、アンマウント時の後片付け忘れが思わぬ不具合やメモリリークの原因になることがあります。

「動いてるからヨシ」ではなく、「不要な処理が残ってないか?」という視点でコードを見直す習慣をつけておくことで、より安全で快適なアプリを作っていきましょう。

Discussion