T-CREATOR

Apollo キャッシュ操作チートシート:`cache.modify`/`writeQuery`/`readFragment` 早見表

Apollo キャッシュ操作チートシート:`cache.modify`/`writeQuery`/`readFragment` 早見表

Apollo Client でキャッシュを操作する際、「どの API を使えばいいのか」「それぞれ何が違うのか」で迷ったことはありませんか?

この記事では、Apollo Client が提供する主要なキャッシュ操作 API である cache.modifywriteQueryreadFragment使い分け方実践的なコード例 をチートシート形式でお届けします。各 API の特徴を理解することで、適切な場面で適切な方法を選べるようになり、キャッシュ操作がぐっと楽になりますよ。

Apollo Client キャッシュ操作 API 早見表

#API主な用途操作対象戻り値注意点
1cache.modifyフィールド単位の更新・削除特定オブジェクトのフィールド真偽値INVALIDATE で削除可能
2cache.writeQueryクエリ結果全体の書き込みクエリ全体Reference既存データを完全上書き
3cache.readQueryクエリ結果の読み取りクエリ全体データまたは nullキャッシュミス時は null
4cache.writeFragmentフラグメント単位の書き込み特定オブジェクトの一部Referenceid 必須、部分更新向き
5cache.readFragmentフラグメント単位の読み取り特定オブジェクトの一部データまたは nullid 必須、軽量読み取り

各 API の特徴比較表

#項目cache.modifywriteQuery / readQuerywriteFragment / readFragment
1粒度フィールド単位クエリ全体フラグメント単位
2型安全性低(関数ベース)高(GraphQL 型)高(GraphQL 型)
3部分更新★★★★☆☆★★☆
4削除操作★★★★☆☆★★☆
5学習コスト
6パフォーマンス高速中速高速

背景

Apollo Client のキャッシュ機構

Apollo Client は、GraphQL のクエリ結果を 正規化されたキャッシュ として保存します。この仕組みにより、同じデータへの複数回のアクセスを効率化し、UI の再レンダリングを最小限に抑えられます。

mermaidflowchart LR
  query["GraphQL クエリ"] -->|実行| apollo["Apollo Client"]
  apollo -->|結果を保存| cache[("正規化<br/>キャッシュ")]
  cache -->|再利用| ui["React コンポーネント"]
  ui -->|更新要求| modify["キャッシュ<br/>操作 API"]
  modify -->|直接変更| cache

図で理解できる要点:

  • Apollo Client はクエリ結果を正規化してキャッシュに保存
  • UI コンポーネントはキャッシュを参照して表示
  • キャッシュ操作 API で直接変更が可能

正規化キャッシュとは

Apollo Client は、各オブジェクトを __typenameid(または _id)の組み合わせで一意に識別します。たとえば、以下のようなクエリ結果があったとします。

typescript// クエリ例
const GET_USER = gql`
  query GetUser {
    user(id: "1") {
      id
      name
      posts {
        id
        title
      }
    }
  }
`;

このクエリ結果は、キャッシュ内で以下のように正規化されます。

typescript// 正規化後のキャッシュイメージ
{
  "User:1": {
    __typename: "User",
    id: "1",
    name: "太郎",
    posts: [{ __ref: "Post:101" }, { __ref: "Post:102" }]
  },
  "Post:101": {
    __typename: "Post",
    id: "101",
    title: "Apollo 入門"
  },
  "Post:102": {
    __typename: "Post",
    id: "102",
    title: "GraphQL 基礎"
  }
}

この構造により、UserPost は独立して管理され、同じ Post が複数のクエリで参照されても、キャッシュ内では 1 つのオブジェクトとして保持されます。

キャッシュ操作が必要になる場面

正規化キャッシュは強力ですが、以下のような場面では 手動でキャッシュを操作 する必要があります。

  • Mutation 後のキャッシュ更新: createPost などの Mutation 実行後、新しいデータをキャッシュに追加したい
  • 楽観的 UI 更新: サーバーレスポンスを待たずに UI を先に更新したい
  • ローカルデータの管理: サーバーに保存しないクライアント専用のデータを管理したい
  • リスト操作の最適化: アイテムの追加・削除時に、リスト全体を再取得せずキャッシュを直接変更したい

これらのケースに対応するため、Apollo Client は複数のキャッシュ操作 API を提供しています。

課題

API が多くて使い分けが難しい

Apollo Client のキャッシュ操作 API は、用途に応じて以下のように分類されます。

#API グループ代表的な API主な用途
1modify 系cache.modifyフィールド単位の更新・削除
2Query 系cache.writeQuerycache.readQueryクエリ全体の読み書き
3Fragment 系cache.writeFragmentcache.readFragment特定オブジェクトの部分的な読み書き
4削除系cache.evictcache.gcキャッシュからオブジェクトを削除

初心者の方にとっては、「どの API をどの場面で使うべきか」が分かりにくいという課題がありますね。

各 API の違いがわからない

たとえば、「ユーザーの名前を更新する」という操作 1 つを取っても、以下のように複数の方法が存在します。

typescript// 方法 1: cache.modify を使う
cache.modify({
  id: cache.identify({ __typename: 'User', id: '1' }),
  fields: {
    name() {
      return '新しい名前';
    },
  },
});
typescript// 方法 2: cache.writeFragment を使う
cache.writeFragment({
  id: cache.identify({ __typename: 'User', id: '1' }),
  fragment: gql`
    fragment UpdateName on User {
      name
    }
  `,
  data: { name: '新しい名前' },
});
typescript// 方法 3: cache.writeQuery を使う(非推奨)
cache.writeQuery({
  query: GET_USER,
  data: {
    user: {
      __typename: 'User',
      id: '1',
      name: '新しい名前',
      posts: [
        /* 既存の posts データ */
      ],
    },
  },
});

このように、複数の選択肢があるため、「どれを使うのがベストプラクティスなのか」が見えにくいですね。

実践的なコード例が少ない

公式ドキュメントには API のリファレンスが記載されていますが、「実際のプロジェクトでどう使うか」という実践例が不足しているケースがあります。特に以下のようなケースでは、コード例が欲しいところです。

  • リストへのアイテム追加・削除
  • ネストされたオブジェクトの更新
  • 複数フィールドの同時更新
  • エラーハンドリング

解決策

用途別に API を使い分ける

Apollo Client のキャッシュ操作 API は、以下の基準で使い分けるのが効果的です。

mermaidflowchart TD
  start["キャッシュ操作<br/>したい"] --> q1{操作対象は?}
  q1 -->|特定フィールドのみ| modify["cache.modify<br/>を使う"]
  q1 -->|オブジェクト全体| q2{既存データは?}
  q2 -->|クエリ結果| query["writeQuery /<br/>readQuery"]
  q2 -->|特定オブジェクト| fragment["writeFragment /<br/>readFragment"]
  q1 -->|削除したい| evict["cache.evict<br/>を使う"]

図で理解できる要点:

  • 操作対象によって最適な API が異なる
  • フィールド単位なら modify、オブジェクト全体なら FragmentQuery 系を使う
  • 削除操作には専用の evict がある

cache.modify の特徴と使い所

cache.modify は、特定のフィールドを関数で変更する API です。以下のような場面で威力を発揮します。

#用途適している理由
1リストへのアイテム追加既存配列に新要素を追加できる
2フィールドの削除INVALIDATE で削除可能
3条件付き更新関数内で条件分岐できる
4複数フィールドの同時更新fields オブジェクトで複数指定可能

基本構文

typescriptimport { InMemoryCache } from '@apollo/client';

// InMemoryCache インスタンス
const cache = new InMemoryCache();
typescript// cache.modify の基本形
cache.modify({
  id: cache.identify({ __typename: 'User', id: '1' }), // 対象オブジェクトの ID
  fields: {
    // 更新したいフィールド名: 更新関数
    name(existingName) {
      return '新しい名前'; // 新しい値を返す
    },
  },
});

id の指定方法

cache.identify は、__typenameid からキャッシュキーを生成します。

typescript// オブジェクトから ID を生成
const userId = cache.identify({
  __typename: 'User',
  id: '1',
});
console.log(userId); // "User:1"
typescript// ROOT_QUERY を操作する場合(クエリのルート)
cache.modify({
  id: 'ROOT_QUERY',
  fields: {
    users(existingUsers = []) {
      // ルートクエリの users フィールドを操作
      return [...existingUsers];
    },
  },
});

fields の更新関数

fields オブジェクト内の各関数は、以下の引数を受け取ります。

typescript// 更新関数のシグネチャ
cache.modify({
  id: 'User:1',
  fields: {
    name(
      existingValue, // 既存の値
      {
        readField,
        DELETE,
        INVALIDATE,
        isReference,
        toReference,
      }
    ) {
      // readField: 他のフィールドを読み取る
      const currentName = readField('name');

      // DELETE: フィールドを削除(非推奨)
      // return DELETE;

      // INVALIDATE: フィールドを無効化(推奨)
      // return INVALIDATE;

      return '新しい値';
    },
  },
});

writeQuery / readQuery の特徴と使い所

writeQueryreadQuery は、クエリ全体の結果を読み書き する API です。

#API用途戻り値
1cache.writeQueryクエリ結果をキャッシュに書き込むReference
2cache.readQueryクエリ結果をキャッシュから読み取るデータまたは null

基本構文

typescriptimport { gql } from '@apollo/client';

// クエリの定義
const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
    }
  }
`;
typescript// writeQuery: クエリ結果を書き込む
cache.writeQuery({
  query: GET_USERS,
  data: {
    users: [
      { __typename: 'User', id: '1', name: '太郎' },
      { __typename: 'User', id: '2', name: '花子' },
    ],
  },
});
typescript// readQuery: クエリ結果を読み取る
const data = cache.readQuery({ query: GET_USERS });
console.log(data?.users); // [{ id: "1", name: "太郎" }, ...]

変数付きクエリの扱い

GraphQL クエリに変数がある場合、variables も指定する必要があります。

typescript// 変数付きクエリ
const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
    }
  }
`;
typescript// 書き込み時に変数を指定
cache.writeQuery({
  query: GET_USER,
  variables: { id: '1' },
  data: {
    user: { __typename: 'User', id: '1', name: '太郎' },
  },
});
typescript// 読み取り時にも同じ変数を指定
const data = cache.readQuery({
  query: GET_USER,
  variables: { id: '1' },
});
console.log(data?.user); // { id: "1", name: "太郎" }

注意点:既存データの上書き

writeQuery既存のクエリ結果を完全に上書き します。部分的な更新には向いていません。

typescript// 既存データ
cache.writeQuery({
  query: GET_USER,
  data: {
    user: {
      __typename: 'User',
      id: '1',
      name: '太郎',
      email: 'taro@example.com',
    },
  },
});
typescript// 部分的に更新しようとすると、他のフィールドが消える
cache.writeQuery({
  query: gql`
    query GetUser {
      user(id: "1") {
        id
        name
      }
    }
  `,
  data: {
    user: { __typename: 'User', id: '1', name: '次郎' },
    // email フィールドが失われる!
  },
});

writeFragment / readFragment の特徴と使い所

writeFragmentreadFragment は、特定のオブジェクトの一部を読み書き する API です。

#API用途戻り値
1cache.writeFragmentフラグメント単位でキャッシュに書き込むReference
2cache.readFragmentフラグメント単位でキャッシュから読み取るデータまたは null

基本構文

typescriptimport { gql } from '@apollo/client';

// フラグメントの定義
const USER_FRAGMENT = gql`
  fragment UserInfo on User {
    id
    name
    email
  }
`;
typescript// writeFragment: フラグメントを書き込む
cache.writeFragment({
  id: cache.identify({ __typename: 'User', id: '1' }),
  fragment: USER_FRAGMENT,
  data: {
    __typename: 'User',
    id: '1',
    name: '太郎',
    email: 'taro@example.com',
  },
});
typescript// readFragment: フラグメントを読み取る
const user = cache.readFragment({
  id: cache.identify({ __typename: 'User', id: '1' }),
  fragment: USER_FRAGMENT,
});
console.log(user); // { id: "1", name: "太郎", email: "taro@example.com" }

部分更新が可能

writeFragment は、指定したフィールドのみを更新 します。他のフィールドは保持されます。

typescript// 既存データ
cache.writeFragment({
  id: 'User:1',
  fragment: gql`
    fragment UserFull on User {
      id
      name
      email
      age
    }
  `,
  data: {
    __typename: 'User',
    id: '1',
    name: '太郎',
    email: 'taro@example.com',
    age: 25,
  },
});
typescript// name だけを更新
cache.writeFragment({
  id: 'User:1',
  fragment: gql`
    fragment UserName on User {
      name
    }
  `,
  data: {
    name: '次郎',
  },
});
typescript// 他のフィールドは保持される
const user = cache.readFragment({
  id: 'User:1',
  fragment: gql`
    fragment UserFull on User {
      id
      name
      email
      age
    }
  `,
});
console.log(user); // { id: "1", name: "次郎", email: "taro@example.com", age: 25 }

フラグメントの再利用

フラグメントは、クエリや Mutation と共有できます。

typescript// フラグメント定義
const USER_FRAGMENT = gql`
  fragment UserInfo on User {
    id
    name
    email
  }
`;
typescript// クエリで使用
const GET_USER = gql`
  ${USER_FRAGMENT}
  query GetUser($id: ID!) {
    user(id: $id) {
      ...UserInfo
    }
  }
`;
typescript// キャッシュ操作でも同じフラグメントを使用
const user = cache.readFragment({
  id: 'User:1',
  fragment: USER_FRAGMENT,
});

具体例

例 1: リストへのアイテム追加(cache.modify)

Mutation で新しい投稿を作成した後、投稿リストに追加する例です。

typescriptimport { gql, useMutation } from '@apollo/client';

// Mutation の定義
const CREATE_POST = gql`
  mutation CreatePost($title: String!, $content: String!) {
    createPost(title: $title, content: $content) {
      id
      title
      content
    }
  }
`;
typescript// カスタムフック内での使用
function useCreatePost() {
  const [createPost] = useMutation(CREATE_POST, {
    update(cache, { data }) {
      // 新しく作成された投稿
      const newPost = data?.createPost;
      if (!newPost) return;

      // キャッシュを変更してリストに追加
      cache.modify({
        id: 'ROOT_QUERY', // ルートクエリを操作
        fields: {
          posts(existingPosts = []) {
            // 新しい投稿への参照を作成
            const newPostRef = cache.writeFragment({
              data: newPost,
              fragment: gql`
                fragment NewPost on Post {
                  id
                  title
                  content
                }
              `,
            });

            // 既存のリストに追加
            return [...existingPosts, newPostRef];
          },
        },
      });
    },
  });

  return createPost;
}
typescript// コンポーネントでの使用例
function CreatePostForm() {
  const createPost = useCreatePost();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    await createPost({
      variables: {
        title: '新しい投稿',
        content: '内容です',
      },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* フォーム要素 */}
    </form>
  );
}

ポイント:

  • ROOT_QUERY を操作してルートレベルのリストを更新
  • writeFragment で新しいオブジェクトへの参照を作成
  • existingPosts にデフォルト値 [] を設定してエラー回避

例 2: リストからのアイテム削除(cache.evict + cache.gc)

投稿を削除する際、キャッシュからも完全に削除する例です。

typescriptimport { gql, useMutation } from '@apollo/client';

// Mutation の定義
const DELETE_POST = gql`
  mutation DeletePost($id: ID!) {
    deletePost(id: $id)
  }
`;
typescript// カスタムフック内での使用
function useDeletePost() {
  const [deletePost] = useMutation(DELETE_POST, {
    update(cache, { data }, { variables }) {
      if (!data?.deletePost) return;

      const postId = variables?.id;
      if (!postId) return;

      // キャッシュから投稿を削除
      cache.evict({
        id: cache.identify({
          __typename: 'Post',
          id: postId,
        }),
      });

      // ガベージコレクションで孤立した参照を削除
      cache.gc();
    },
  });

  return deletePost;
}
typescript// コンポーネントでの使用例
function PostItem({
  post,
}: {
  post: { id: string; title: string };
}) {
  const deletePost = useDeletePost();

  const handleDelete = async () => {
    await deletePost({
      variables: { id: post.id },
    });
  };

  return (
    <div>
      <h3>{post.title}</h3>
      <button onClick={handleDelete}>削除</button>
    </div>
  );
}

ポイント:

  • cache.evict で特定のオブジェクトをキャッシュから削除
  • cache.gc() で参照されなくなったオブジェクトを削除
  • リストからも自動的に削除される

例 3: ネストされたオブジェクトの更新(readFragment + writeFragment)

ユーザーの投稿の 1 つを更新する例です。

typescriptimport { gql, useMutation } from '@apollo/client';

// Mutation の定義
const UPDATE_POST = gql`
  mutation UpdatePost($id: ID!, $title: String!) {
    updatePost(id: $id, title: $title) {
      id
      title
    }
  }
`;
typescript// カスタムフック内での使用
function useUpdatePost() {
  const [updatePost] = useMutation(UPDATE_POST, {
    update(cache, { data }) {
      const updatedPost = data?.updatePost;
      if (!updatedPost) return;

      // 既存の投稿データを読み取る
      const existingPost = cache.readFragment({
        id: cache.identify({
          __typename: 'Post',
          id: updatedPost.id,
        }),
        fragment: gql`
          fragment ExistingPost on Post {
            id
            title
            content
          }
        `,
      });

      // 新しいデータで更新
      if (existingPost) {
        cache.writeFragment({
          id: cache.identify({
            __typename: 'Post',
            id: updatedPost.id,
          }),
          fragment: gql`
            fragment UpdatedPost on Post {
              title
            }
          `,
          data: {
            title: updatedPost.title,
          },
        });
      }
    },
  });

  return updatePost;
}
typescript// コンポーネントでの使用例
function EditPostForm({ postId }: { postId: string }) {
  const updatePost = useUpdatePost();
  const [title, setTitle] = React.useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    await updatePost({
      variables: { id: postId, title },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
      />
      <button type='submit'>更新</button>
    </form>
  );
}

ポイント:

  • readFragment で既存データを確認
  • writeFragment で必要なフィールドのみを更新
  • 他のフィールド(content など)は保持される

例 4: 複数フィールドの同時更新(cache.modify)

ユーザーの複数のフィールドを一度に更新する例です。

typescriptimport { gql, useMutation } from '@apollo/client';

// Mutation の定義
const UPDATE_USER = gql`
  mutation UpdateUser(
    $id: ID!
    $name: String!
    $email: String!
  ) {
    updateUser(id: $id, name: $name, email: $email) {
      id
      name
      email
      updatedAt
    }
  }
`;
typescript// カスタムフック内での使用
function useUpdateUser() {
  const [updateUser] = useMutation(UPDATE_USER, {
    update(cache, { data }) {
      const updatedUser = data?.updateUser;
      if (!updatedUser) return;

      // 複数フィールドを同時に更新
      cache.modify({
        id: cache.identify({
          __typename: 'User',
          id: updatedUser.id,
        }),
        fields: {
          name() {
            return updatedUser.name;
          },
          email() {
            return updatedUser.email;
          },
          updatedAt() {
            return updatedUser.updatedAt;
          },
        },
      });
    },
  });

  return updateUser;
}
typescript// コンポーネントでの使用例
function UserProfileForm({ userId }: { userId: string }) {
  const updateUser = useUpdateUser();
  const [name, setName] = React.useState('');
  const [email, setEmail] = React.useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    await updateUser({
      variables: { id: userId, name, email },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        placeholder='名前'
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <input
        placeholder='メール'
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <button type='submit'>更新</button>
    </form>
  );
}

ポイント:

  • fields オブジェクトで複数フィールドを一度に指定
  • 各フィールドに対して関数を定義
  • 他のフィールドに影響を与えずに更新可能

例 5: 条件付き更新(cache.modify + readField)

いいね数を増やす際、既存の値を読み取って条件付きで更新する例です。

typescriptimport { gql, useMutation } from '@apollo/client';

// Mutation の定義
const LIKE_POST = gql`
  mutation LikePost($id: ID!) {
    likePost(id: $id) {
      id
      likes
      isLiked
    }
  }
`;
typescript// カスタムフック内での使用
function useLikePost() {
  const [likePost] = useMutation(LIKE_POST, {
    update(cache, { data }) {
      const likedPost = data?.likePost;
      if (!likedPost) return;

      cache.modify({
        id: cache.identify({
          __typename: 'Post',
          id: likedPost.id,
        }),
        fields: {
          likes(existingLikes = 0, { readField }) {
            // 既に「いいね」済みかチェック
            const isLiked = readField('isLiked');

            if (isLiked) {
              // すでに「いいね」済みなら変更しない
              return existingLikes;
            }

            // 「いいね」数を増やす
            return existingLikes + 1;
          },
          isLiked() {
            return true;
          },
        },
      });
    },
  });

  return likePost;
}
typescript// コンポーネントでの使用例
function LikeButton({ postId }: { postId: string }) {
  const likePost = useLikePost();

  const handleLike = async () => {
    await likePost({
      variables: { id: postId },
    });
  };

  return <button onClick={handleLike}>いいね</button>;
}

ポイント:

  • readField で他のフィールドの値を参照
  • 条件分岐で重複操作を防止
  • 複数フィールドを連動して更新

例 6: 楽観的 UI 更新(optimisticResponse + cache.modify)

サーバーレスポンスを待たずに UI を先に更新する例です。

typescriptimport { gql, useMutation } from '@apollo/client';

// Mutation の定義
const CREATE_COMMENT = gql`
  mutation CreateComment($postId: ID!, $content: String!) {
    createComment(postId: $postId, content: $content) {
      id
      content
      createdAt
      author {
        id
        name
      }
    }
  }
`;
typescript// カスタムフック内での使用
function useCreateComment(currentUser: {
  id: string;
  name: string;
}) {
  const [createComment] = useMutation(CREATE_COMMENT, {
    // 楽観的レスポンス:サーバーからの応答を待たずに UI を更新
    optimisticResponse: (variables) => ({
      __typename: 'Mutation',
      createComment: {
        __typename: 'Comment',
        id: `temp-${Date.now()}`, // 一時的な ID
        content: variables.content,
        createdAt: new Date().toISOString(),
        author: {
          __typename: 'User',
          id: currentUser.id,
          name: currentUser.name,
        },
      },
    }),

    update(cache, { data }, { variables }) {
      const newComment = data?.createComment;
      if (!newComment) return;

      cache.modify({
        id: cache.identify({
          __typename: 'Post',
          id: variables.postId,
        }),
        fields: {
          comments(existingComments = []) {
            const newCommentRef = cache.writeFragment({
              data: newComment,
              fragment: gql`
                fragment NewComment on Comment {
                  id
                  content
                  createdAt
                  author {
                    id
                    name
                  }
                }
              `,
            });

            return [...existingComments, newCommentRef];
          },
        },
      });
    },
  });

  return createComment;
}
typescript// コンポーネントでの使用例
function CommentForm({ postId }: { postId: string }) {
  const currentUser = { id: '1', name: '太郎' }; // 実際はログインユーザーを取得
  const createComment = useCreateComment(currentUser);
  const [content, setContent] = React.useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    await createComment({
      variables: { postId, content },
    });

    setContent('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
      />
      <button type='submit'>コメント</button>
    </form>
  );
}

ポイント:

  • optimisticResponse で即座に UI を更新
  • サーバーからの応答後、一時的な ID が正式な ID に置き換わる
  • ネットワーク遅延があっても、ユーザーはすぐに結果を確認できる

例 7: エラーハンドリング(readQuery のエラー処理)

キャッシュにデータが存在しない場合のエラーハンドリング例です。

typescriptimport {
  gql,
  useMutation,
  ApolloCache,
} from '@apollo/client';

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
    }
  }
`;
typescriptconst ADD_USER = gql`
  mutation AddUser($name: String!) {
    addUser(name: $name) {
      id
      name
    }
  }
`;
typescriptfunction useAddUser() {
  const [addUser] = useMutation(ADD_USER, {
    update(cache: ApolloCache<any>, { data }) {
      const newUser = data?.addUser;
      if (!newUser) return;

      try {
        // キャッシュからクエリ結果を読み取る
        const existingData = cache.readQuery({
          query: GET_USERS,
        });

        if (existingData?.users) {
          // キャッシュが存在する場合、新しいユーザーを追加
          cache.writeQuery({
            query: GET_USERS,
            data: {
              users: [...existingData.users, newUser],
            },
          });
        }
      } catch (error) {
        // キャッシュが存在しない場合、modify で直接追加
        console.warn(
          'キャッシュにクエリ結果がありません。modify で追加します。'
        );

        cache.modify({
          id: 'ROOT_QUERY',
          fields: {
            users(existingUsers = []) {
              const newUserRef = cache.writeFragment({
                data: newUser,
                fragment: gql`
                  fragment NewUser on User {
                    id
                    name
                  }
                `,
              });

              return [...existingUsers, newUserRef];
            },
          },
        });
      }
    },
  });

  return addUser;
}
typescript// コンポーネントでの使用例
function AddUserForm() {
  const addUser = useAddUser();
  const [name, setName] = React.useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    await addUser({
      variables: { name },
    });

    setName('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        placeholder='名前'
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <button type='submit'>追加</button>
    </form>
  );
}

ポイント:

  • readQuerynull を返す場合があるため try-catch で囲む
  • キャッシュが存在しない場合は modify にフォールバック
  • エラーハンドリングで堅牢なコードを実現

まとめ

Apollo Client のキャッシュ操作 API は、用途に応じて適切に使い分けることで、効率的かつ堅牢なキャッシュ管理が実現できます。

各 API の特徴をおさらいしましょう。

#API最適な用途注意点
1cache.modifyフィールド単位の更新、リスト操作readField で他フィールド参照可能
2cache.writeQueryクエリ全体の書き込み既存データを完全上書き
3cache.readQueryクエリ結果の読み取りキャッシュミス時は null が返る
4cache.writeFragment特定オブジェクトの部分更新id が必須、部分更新向き
5cache.readFragment特定オブジェクトの読み取り軽量で効率的
6cache.evictオブジェクトの削除gc() と併用推奨

この記事で紹介したコード例を参考に、皆さんのプロジェクトでもキャッシュ操作を実践してみてください。適切な API を選ぶことで、パフォーマンスが向上し、コードの可読性も高まりますよ。

Apollo Client のキャッシュ機構を活用して、快適な GraphQL 開発を楽しんでくださいね。

関連リンク