React.js

ReactのuseApiData:API通信のloading・error・再取得を管理する

この記事でわかること

API取得を独自Hookへ切り出し、読み込み・失敗・成功、中止、古い通信結果の抑止、再取得を実装します。

API取得を独自Hookへ切り出し、読み込み・失敗・成功、中止、古い通信結果の抑止、再取得を実装します。

useApiDataは何をまとめるか

GETの通信状態を返すカスタムフック

useApiData は自作のHookです。URLに応じたJSON取得と、loadingerrordatareload をまとめます。fetchJsonの記事src/api/fetchJson.ts を先に用意してください。ViteのReact+TypeScript構成で動かす例です。

URL変更時には以前のリクエストを中止し、古い結果で表示を上書きしないようにします。Effectは外部との同期を担当し、終了時には対応する後始末を行います。ReactのuseEffect解説では依存関係やクリーンアップが説明されています。

src/hooks/useApiData.tsの実装

URLと再取得回数を依存値にする

TypeScript
import { useEffect, useState } from 'react';
import { fetchJson } from '../api/fetchJson';

type Result = {
  url: string;
  data: unknown;
  error: string | null;
  loading: boolean;
};
export function useApiData(url: string) {
  const [version, setVersion] = useState(0);
  const [result, setResult] = useState<Result>({
    url, data: null, error: null, loading: true,
  });
  useEffect(() => {
    const controller = new AbortController();
    let active = true;
    setResult({ url, data: null, error: null, loading: true });
    fetchJson(url, controller.signal).then(
      data => {
        if (active) setResult({ url, data, error: null, loading: false });
      },
      cause => {
        if (active) setResult({
          url, data: null, loading: false,
          error: cause instanceof Error ? cause.message : '取得に失敗しました',
        });
      },
    );
    return () => {
      active = false;
      controller.abort();
    };
  }, [url, version]);
  const current = result.url === url ? result : {
    url, data: null, error: null, loading: true,
  };
  function reload() { setVersion(value => value + 1); }
  return { ...current, reload };
}

active はそのEffectが現在も有効かを示し、後始末後の成功・失敗結果を無視します。中止だけに依存せず、state更新にも条件を付けています。さらに結果へURLを保存し、URL変更直後のレンダーで前のURLの内容を表示しないようにしています。URLは空文字列でない有効な取得先を渡してください。

コンポーネントから利用する

src/App.tsx

前の記事と同じ public/api/message.json を配置します。Hookの戻り値は未検証データなので、画面側で必要なフィールドを確認します。

TSX
import { useApiData } from './hooks/useApiData';

export default function App() {
  const { data, error, loading, reload } = useApiData('/api/message.json');
  const message = typeof data === 'object' && data !== null &&
    'message' in data && typeof data.message === 'string'
    ? data.message : null;
  return <main>
    <h1>通信状態を管理する</h1>
    <button type="button" onClick={reload} disabled={loading}>再取得</button>
    {loading ? <p role="status">読み込み中</p>
      : error ? <p role="alert">{error}</p>
      : message === null ? <p role="alert">データ形式が不正です</p>
      : <p>{message}</p>}
  </main>;
}

null は未取得・失敗でも使うため、値だけで成功を判定しません。表示はまず loading、次に error、最後にデータ内容の順で分けます。空のメッセージも正常な文字列として扱います。

競合・Strict Mode・再取得の確認

遅い通信を使って確認する

開発者ツールで通信速度を遅くし、取得中に対象コンポーネントを非表示にしてください。後始末によって中止が要求されます。URLをAからBへ変更した場合は、Aが遅れて完了してもBの結果を上書きしないことを確認します。Strict Modeの開発時の追加実行でも、開始と後始末を対にします。

小さなHookの限界を理解する

この例にはキャッシュ、重複リクエストの集約、リトライ、タイムアウト、SSRでの事前取得はありません。複数箇所で同じURLを使うと通信とstateも別です。要件が増えたらフレームワークの取得機能やデータ取得ライブラリを比較します。Effect内部からPOSTして購入や登録を実行せず、送信イベントの処理として設計します。

PREFERRED SOURCES

Googleの優先するニュース提供元にSEの部屋を追加

Googleで、いつも読みたい情報源を選べます。登録可否はGoogleの画面で確認できます。

Googleの設定画面で確認する 新しいタブで開きます

候補にSEの部屋が表示されない場合は、まだ追加できません。

y.
WRITTEN BY

y_ymo10

SEの部屋で、HTML・CSSからJavaScript・React・Next.jsまで、Web制作の開発ノートを公開しています。

ほかの開発ノートを読む →