react-queryをリコーディング | Reactでの非同期データ取得＆キャッシングのための軽量フックの紹介

皆さん、こんにちは！

私はReact Queryの基本的な機能（フェッチ、キャッシング、リトライなど）を模倣しつつ、よりコンパクトでカスタマイズしやすいパッケージとして軽量なReactフックに取り組んでいました。以下に、その内部動作の簡単な概要を、関連するコードセクションを参照しながら説明します。完全なコードを見たい場合は、リポジトリをご覧ください：  
**[GitHubのフルソースコード](https://github.com/aymericzip/api-refetch)**。  
このフックはnpmでも利用可能です：**[`api-refetch`](https://www.npmjs.com/package/api-refetch)**。

![Screenshot 2025-01-07 at 11.13.01 am.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963127/8aab0020-2ab2-4668-4e54-d96ca5acb01c.png)

---

## なぜ自作のフックを作ったのか？

React QueryやSWRはどちらも優れたライブラリですが、以下の理由からよりハンズオンなアプローチを取りたかったのです：

1. **軽量なフットプリント**  
   React QueryやSWRは機能が豊富ですが、比較的大きくなりがちです。パッケージサイズが重要な場合、自分自身のフックを作成するのが理想的です。このフックは他のライブラリ（[Intlayer](https://intlayer.org)）の依存関係としてインストールされることを意図しているため、ソリューションのサイズが重要な考慮事項となりました。

2. **カスタマイズと最適化の容易さ**  
   **ローカルストレージからのデータの保存/取得**や**単純なアプローチによる並列リクエストの管理**など、特定の機能が必要でした。  
   **リポジトリをクローン**するか**コードを直接プロジェクトにコピー**することで、不要な機能を削除し、必要なものだけを保持できます。これにより、**バンドルサイズが削減**されるだけでなく、不要な再レンダリングが最小限に抑えられ、特定の要件に合わせたより軽量でパフォーマンスの高いソリューションが実現します。

3. **プロバイダーが不要**  
   グローバルにフックを利用するためにContext Providerを避け、使用をできるだけシンプルにしたかったです。そのため、Zustandストアをベースにしたバージョンのフックを作成しました（以下の例を参照）。

4. **学習のための演習**  
   非同期ライブラリをゼロから構築することは、**同時実行性、キャッシング、状態管理の内部を理解するための優れた方法**です。

要するに、自分自身のフックを作成することで、**必要な機能に集中し**（不要なものはスキップし）、ライブラリを小さく保ち、理解しやすくすることができました。

## カバーされている機能

このReactフックは以下を管理します：

- **フェッチと状態管理**: ローディング、エラー、成功、フェッチ済みの状態を処理します。
- **キャッシングとストレージ**: データをオプションでキャッシュ（Reactの状態やZustandを使用）し、[ローカルストレージ](https://developer.mozilla.org/fr/docs/Web/API/Window/localStorage)のサポートを提供します。
- **リトライと再検証**: 設定可能なリトライ制限と自動再検証の間隔。
- **アクティベーションと無効化**: 他のクエリや状態に応じてクエリを自動的にアクティブ化および無効化します。例：ユーザーがログインしたときに自動的にデータをフェッチし、ログアウトしたときに無効化します。
- **並列コンポーネントマウントフェッチ**: 複数のコンポーネントが同時にマウントされたときに、同じリソースに対する複数の同時リクエストを防ぎます。

## GitHubでフルソースを確認

ローカルストレージのロジック、クエリの無効化などを含む完全なコードをこちらで確認できます：

- [**フルソースコード**](https://github.com/aymericzip/api-refetch)

ぜひ試してみてください。問題の報告や貢献も歓迎します。フィードバックは大変ありがたいです！

## 使用例

### インストール

コードをコピーするか、[リポジトリ](https://github.com/aymericzip/api-refetch)をコードしてください。

または

```bash
npm install api-refetch
```

### クイック例

```jsx
// 1. プロバイダーでアプリをラップします（オプションですが推奨）
import { AsyncStateProvider, useAsync } from "api-refetch";
// import { useAsync } from "api-refetch/zustand"; // 代わりにZustandベースのフックを使用（プロバイダーは不要）

function App() {
  return (
    <AsyncStateProvider>
      <UserDetails />
    </AsyncStateProvider>
  );
}

// 2. データをフェッチする非同期関数を定義します
const fetchUserData = async (): Promise<string> => {
  // 1秒後に解決するPromiseを作成
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        name: "John Doe",
      });
    }, 1000);
  });
};

// 3. コンポーネントでフックを使用します
const UserDetails = () => {
  const { isLoading, data, error, revalidate } = useAsync(
    "userDetails",
    async () => await fetchUserData(),
    {
      enable: true, // フックを有効にする
      cache: true, // Zustendを使用してAPIコールの結果をキャッシュする
      store: true, // APIコールの結果をローカルストレージに保存する
      retryLimit: 3, // APIコールが失敗した場合に3回リトライする
      retryTime: 10 * 1000, // リトライする前に10秒待つ
      autoFetch: true, // コンポーネントがマウントされたときにAPIコールを自動的にフェッチする
      revalidation: true, // 再検証を有効にする
      revalidateTime: 5 * 60 * 1000, // 5分ごとに再検証する
      isInvalidated: false, // データが無効化されているかどうかを判断し、再フェッチするかを決定する
      invalidateQueries: ["user"], // データが更新されたときに他のクエリを無効化する
      updateQueries: ["user"], // データが更新されたときに他のクエリのデータを設定する
      onSuccess: (data) => console.log("ユーザーデータが正常に取得されました:", data),
      onError: (error) => console.error("ユーザーデータの取得中にエラーが発生しました:", error),
    }
  );

  if (isLoading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  return (
    <div>
      <h1>{data?.name}</h1>
      <button onClick={() => revalidate()}>更新</button>
    </div>
  );
};
```

---

以上です！ぜひ試してみてください。フィードバック、質問、貢献はGitHubで大歓迎です。

**GitHub:** [api-refetch](https://github.com/aymericzip/api-refetch)

ハッピーコーディング！