【Next.js】SWRとaspidaでフロントエンドのデータ取得部分を改善した話

こんにちは、ちいき資本主義事業部でフロントエンドエンジニアをしている片岡です。

この記事ではカヤックが開発している「まちのコイン」の管理ダッシュボードで、APIサーバからのデータ取得部分の改善したことについて紹介します。

まちのコインについては、以下のURLをご覧ください。

coin.machino.co

管理ダッシュボードについて

dev環境のダッシュボードのスクリーンショット画像

管理ダッシュボードは、まちのコインが導入されている地域の運営団体の方が主に利用します。ダッシュボードには、統計情報の確認やお知らせの配信、まちのコインの体験を作成できる機能があります。

管理ダッシュボード開発の課題

ダッシュボードは、Next.js(React)で開発をしていて状態管理ライブラリはRecoilを採用しています。

ReactコンポーネントでAPIを叩いてデータ取得をする場合は、React HooksのuseEffect()で非同期関数を呼び出してレスポンスをRecoilのatomに書き込んで各コンポーネントから参照するという実装をしてしました。

例として、ユーザ一覧を表示するコンポーネントでは、以下のようにしてAPIを叩いていまいた。

import { useState, useEffect } from "react";

const App = () => {
  // サンプルのため、recoilの代わりにuseStateで表現
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetch("https://api.example.com/users").then((res) => setUsers(res.json()));
  }, []);

  return (
    <div>
      <ul>
        {users.map((user, index) => (
          <li key={index}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default App;

これで開発当初は問題なかったのですが、いくつかの課題がありました。

  • 検索などでクエリパラメータをつけてAPIを叩く時、useEffect()の依存配列が増えて副作用が複雑になってしまう
  • hooksの実行タイミングの制御が難しい
  • データ取得前のローディング表示のステート管理
  • エラーハンドリング、リトライ処理

これらを実装しようとすると非常に煩雑になります。また、コンポーネントはレスポンスをRecoilのatom経由で参照していたので、エンドポイントの数だけatomを管理している状況で、徐々に実装コストが高くなり開発体験が下がっていました。

SWRとaspidaでAPIを叩く

データ取得部分の課題を解決するために2つのライブラリを導入しました。

SWR

swr.vercel.app

SWRはNext.jsを開発しているVercel社が作成しているデータフェッチのReact hooksライブラリです。レスポンスデータをJavaScriptのメモリにキャッシュして扱ってくれる便利ライブラリで、キャッシュ戦略はstale while revalidateを採用しています。

useSWRというReact Hooksを提供していて、データ取得、ローディング状態、エラー時の表示をシンプルに記述することができます。また一度取得したデータをキャッシュするので、コンポーネント間でデータをpropsする必要はなく各コンポーネントでuseSWRを使うことができます。

aspida

github.com

aspidaは、APIに型を付けることができるHTTPクライアントライブラリです。今までのAPIの型定義は、ファイル名が似たものが多くて分かりにくかったのですがaspidaは、エンドポイントをプロパティとメソッドで表現しているので管理がすごく楽になりました。

SWRをラップしたライブラリもあります。

www.npmjs.com

開発時は、エンドポイントがプロパティとなることで補完されることや、クエリパラメータも補完されるので開発がしやすいです。

以下は、ユーザ一覧を取得するGETメソッドの定義をしたコードです。レスポンスの型の他に、クエリパラメータやヘッダーの型も定義できます。 aspidaを使用するときはMethodsという命名で型をexportします。 exportした型からaspidaはAPI型定義$apis.tsを生成してくれるので@aspida/swruseAspidaSWRを介してHTTPリクエストをすることができます。

// api/users/index.ts

export type Methods = {
  get: {
    // リクエストヘッダー
    reqHeaders: {
      "x-api-key": string;
    };

    // レスポンスヘッダー
    resHeaders: {
      total: number;
    };

    // クエリパラメータ
    query: {
      page: number;
    };

    // レスポンス
    resBody: {
      users: {
        name: string;
        id: string;
      }[];
    };
  };
};

実装例

SWRを使うことで実装がシンプルになりました。

import useAspidaSWR from "@aspida/swr";
import { apiClient } from "~/lib/api";

const App = () => {
  const { data, error } = useAspidaSWR(apiClient.users);

  if (error) {
    return <div>エラーが発生しました</div>;
  }

  if (!data) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <ul>
        {data.users.map((user, index) => (
          <li key={index}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default App;

useAspidaSWRの第1引数には、aspidaで生成したクライアントを指定します。第2引数には、クエリパラメータやSWRのオプションを書くことでそのままcache keyとして渡されます。

おわりに

SWRとaspidaを開発しているアプリケーションに導入したことで、開発体験が改善された手応えを感じました。また、従来の実装よりもデータ表示速度が高速になったり、ローディング表示のストレスが軽減されたので利用者の操作性も向上させることができました。

導入前は、recoilのatomが137個、関連するファイルは80個ありましたが、導入後は、どちらも半分の数まで減らすことができました。

SWRは、エラー時の自動リトライやポーリングによってユーザに最新のデータを表示することができることが強みだと思いました。キャッシュの更新タイミングの制御は、オプションを使いこなすのが多少難しく感じました。

aspidaを使うことでプロパティのタイポを防ぐことができます。APIのパスが変更になった場合でも定義を変更すれば型エラーで修正箇所が分かるのも便利な点だと感じました。

今回は丁寧に型を書いていましたが、OpenAPIが整備されていれば自動で型情報を生成してくれるみたいです。今後は、OpenAPIから生成できるようにしたいと考えています。

最後に

ちいき資本主義事業部ではエンジニアを募集しています。興味のある方は下記のバナーからどうぞ。

www.kayac.com

hubspot.kayac.com