RevComm Tech Blog

コミュニケーションを再発明し 人が人を想う社会を創る

Apollo Clientを活用した効率的なGraphQLデータ管理とキャッシュ運用

はじめに

こんにちは!

RevComm のフロントエンドエンジニアの楽桑です。

私たちのコールセンターシステムでは、GraphQL を使用してデータを管理しており、これまではRecoil を使ってローカルステートを管理していました。

最近では、Recoil の代わりにApollo ClientLocal Cache を採用し、サーバーデータの取得・管理をより簡潔かつ効率的に行っています。

この記事では、Apollo Client のキャッシュ利用について紹介します。

背景

今まではRecoilを使ってグローバルな状態管理を行ってきました。Recoilには以下のようなメリットがあります:

  • 使いやすさ:シンプルで直感的に状態管理が可能。
  • 学習コストが低い:初学者でも簡単に扱える設計。

プロジェクト初期の段階においては、これらのメリットを活かして素早く状態管理を整えることができ、Recoil は悪くない選択肢でした。

しかし、サーバーから取得したデータを管理するケースにおいては、以下のような課題が浮き彫りになりました:

  1. データ同期の手動実装が必要

    サーバーから取得したデータをローカルの Recoil 状態と同期させるには、追加の実装が必要です。これにより、コードの冗長化や保守性の低下を招きます。

  2. データ取得の効率化が困難

    同じデータを複数のコンポーネントで使用する場合、無駄なAPIリクエストが発生しやすく、パフォーマンスが低下します。

  3. 最新データの取得とパフォーマンスのトレードオフ

    リアルタイム性が求められる場合、常にサーバーからデータを取得する実装ではパフォーマンスの劣化を避けられません。

こうした背景から、サーバーと連携した効率的な状態管理を実現するために、Apollo Clientの導入を決めました。Apollo Client は、GraphQL の強力なキャッシュ管理を活用し、データ取得の効率化と同期の手間を軽減することで、これらの課題を解決します。

Apollo Client Cache とは

Apollo Client Cacheは、GraphQLを使ったデータ取得の効率を最大化するためのキャッシュ機能です。

一度取得したデータをクライアント側に保存し、再利用することでネットワークリクエストの削減など多くメリットある強力な機能です。

実装例

これまでのRecoilを用いた実装では、まずRecoil Atomを定義するところから始める必要がありました。

export const userState = atom({
    key: 'userState',
    default: [],
});

たとえば useGetUser などのフックを定義する場合、データ取得が成功したタイミングでonCompleted コールバック内から手動で Recoil State へデータをセットする必要があります。

const useFetchUsersWithRecoil = () => {

const [users, setUsers] = useRecoilState(userState);

const [getUsers, { data, loading, error }] = useLazyQuery(GET_USERS,{

    fetchPolicy: 'network-only',

    onCompleted: (data) => {

            setUsers(data.users);

        }

    });

    return { getUsers, users, loading, error };

};

また、Recoil State を更新するたびに再レンダリングが発生するため、useLazyQuery を用いて取得回数を必要最低限に抑える必要があるなど、いくつかのデメリットも存在します

一方、Apollo Client のローカルキャッシュ機能(Local Cache)のみを用いる場合、Recoil Stateの定義や初期設定といった手順は不要になります。

const useFetchUsersWithApollo = () => {
  const { data, loading, error } = useQuery(GET_USERS, {
    fetchPolicy: 'cache-first',
  });

  const users = data?.users ?? [];

  return { users, loading, error };
};

fetchPolicycache-first に設定すると、Apollo Client はすでにローカルキャッシュ上に存在するデータを優先的に返し、サーバーへの新規リクエストを行わなくなります。

これは、同じデータを何度も取得する必要がない場面でのパフォーマンス最適化につながり、不要なネットワーク通信を削減することが可能になります。

サブスクリプション動作

WebSocket を使用してデータの更新をサブスクリプションで行う場合の例をご紹介します。

従来の Recoil State を使ったデータ更新では、手動で setState を呼び出す必要がありました。以下はその実装例です:

export const useUserSubscription = () => {
  const [users, setUsers] = useRecoilState(usersState); // Users配列の状態を取得・更新

  useSubscription(USER_UPDATED_SUBSCRIPTION, {
    onSubscriptionData: ({ subscriptionData }) => {
      if (subscriptionData.data?.userUpdated) {
        const updatedUser = subscriptionData.data.userUpdated;

        // 現在のusersを直接参照して更新
        const updatedUsers = users.map((user) =>
          user.id === updatedUser.id ? { ...user, ...updatedUser } : user
        );

        setUsers(updatedUsers);
      }
    },
  });
};

一方、Apollo Client が提供する Local Cache を利用する場合、コードは非常に簡潔になります。

以下はその例です:

export const useUserSubscription = () => {
  useSubscription(USER_UPDATED_SUBSCRIPTION);
};

そして、特定のユーザー情報を更新する場合、CachekeyFields を追加することで、Apollo Client はそのユーザーのキャッシュのみを自動的に更新することが可能です。

export const graphqlCache = new InMemoryCache({
  typePolicies: {
    User: {
      keyFields: [account_id]
      }
    }
 })

このように、サブスクリプションデータの更新時に特定の副作用(Side Effect)が必要ない場合、非常にシンプルな実装が可能です。

カスタムキャッシュマージ

Apollo Client では、フェッチしたデータがキャッシュ上に既に存在する場合、そのデータをどのように更新・統合(マージ)するかを柔軟に制御することができます。

これを typePoliciesmerge 関数を用いて実現できます。

今回は、ユーザー情報のマスキングを例として挙げます。

たとえば、ユーザー情報を管理者のみが閲覧できる仕様にしたい場合、権限判定に応じたマスキング処理をフロントエンド側で行うケースを考えてみます。

まず、ログイン中のユーザー情報を取得し、管理者かどうかを判定します。

管理者であれば、すべてのユーザー情報を開示しますが、管理者でない場合は、maskUser 関数を使用して隠したい情報をマスキングするように実装します。

export const graphqlCache = new InMemoryCache({
  typePolicies: {
    User: {
      keyFields: [account_id]

      merge(
        existing,
        incoming,
        { cache }
      ) {
        const myAccount = cache.readQuery<MyAccountQuery>({
          query: GET_MY_ACCOUNT,
        })?.myAccount;

        if (!myAccount) {
          return incoming;
        }

        const { account_id, permissions } = myAccount;

        const isAdmin = permissions?.includes('is_admin') || false;

        if (isAdmin) {
          return incoming;
        }

        const maskedUser = maskingUser(incoming, account_id);

        return {
          ...existing,
          ...maskedUser,
        };
      },
    },
  },
});

このように、サーバーからユーザーのデータが更新される際に、キャッシュを更新する動作をカスタマイズすることができます。

終わり

最後までお読みいただきありがとうございます!

この記事では、Apollo Client を活用した GraphQL キャッシュの活用方法について解説しました。Recoil から Apollo Client への移行を通じて、サーバーデータの効率的な取得やキャッシュ管理がどれほど便利かをご理解いただけたと思います。

Apollo Client の強力なキャッシュ機能は、単なるデータ取得の効率化だけでなく、アプリケーション全体のパフォーマンス向上やコードの保守性の向上にも寄与します。

特に、カスタムキャッシュマージを活用することで、フロントエンドでの柔軟なデータ操作が可能になります。

プロジェクトの要件によって最適な状態管理ツールは異なりますが、サーバーデータとの連携が必要な場合、Apollo Client は非常に強力な選択肢です。

この記事が、皆さんのアプリケーション開発の参考になれば幸いです。