<CodeLearn/>
GraphQL レッスン4

Apollo Client

ReactからGraphQLを使ってデータを取得・更新しよう

Apollo Client のセットアップ

Apollo Clientは、ReactアプリからGraphQL APIに接続するための クライアントライブラリです。データの取得、キャッシュ管理、状態管理を一元的に行えます。

// パッケージのインストール
// npm install @apollo/client graphql

// lib/apollo-client.ts
import {
  ApolloClient,
  InMemoryCache,
  HttpLink,
} from "@apollo/client";

const client = new ApolloClient({
  link: new HttpLink({
    uri: "http://localhost:4000/graphql",
    headers: {
      authorization: `Bearer ${getToken()}`,
    },
  }),
  cache: new InMemoryCache(),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: "cache-and-network",
    },
  },
});

export default client;

// app/layout.tsx(またはProviderコンポーネント)
"use client";
import { ApolloProvider } from "@apollo/client";
import client from "@/lib/apollo-client";

export function ApolloWrapper({ children }: { children: React.ReactNode }) {
  return (
    <ApolloProvider client={client}>
      {children}
    </ApolloProvider>
  );
}

// app/layout.tsx で使用
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ApolloWrapper>{children}</ApolloWrapper>
      </body>
    </html>
  );
}

useQuery でデータを取得する

useQueryフックは、コンポーネントのレンダリング時に 自動的にGraphQLクエリを実行し、データ、ローディング状態、エラーを返します。

import { gql, useQuery } from "@apollo/client";

// クエリの定義
const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`;

// 変数付きクエリ
const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`;

// コンポーネントで使用
function UserList() {
  const { data, loading, error, refetch } = useQuery(GET_USERS);

  if (loading) return <p>読み込み中...</p>;
  if (error) return <p>エラー: {error.message}</p>;

  return (
    <div>
      <button onClick={() => refetch()}>更新</button>
      {data.users.map((user) => (
        <div key={user.id}>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
          <p>投稿数: {user.posts.length}</p>
        </div>
      ))}
    </div>
  );
}

// 変数を渡すパターン
function UserProfile({ userId }: { userId: string }) {
  const { data, loading, error } = useQuery(GET_USER, {
    variables: { id: userId },
    skip: !userId,            // userId がない場合はスキップ
    pollInterval: 30000,      // 30秒ごとに自動再取得
  });

  if (loading) return <p>読み込み中...</p>;
  if (error) return <p>エラー: {error.message}</p>;
  if (!data?.user) return <p>ユーザーが見つかりません</p>;

  return (
    <div>
      <h2>{data.user.name}</h2>
      <p>{data.user.email}</p>
    </div>
  );
}

useMutation でデータを変更する

useMutationフックは、データの作成・更新・削除に使います。 useQueryと違い、自動実行ではなく、返された関数を呼び出して明示的に実行します。

import { gql, useMutation } from "@apollo/client";

const CREATE_USER = gql`
  mutation CreateUser($input: CreateUserInput!) {
    createUser(input: $input) {
      id
      name
      email
    }
  }
`;

const DELETE_USER = gql`
  mutation DeleteUser($id: ID!) {
    deleteUser(id: $id)
  }
`;

function CreateUserForm() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  const [createUser, { loading, error }] = useMutation(CREATE_USER, {
    // キャッシュの更新(新しいユーザーをリストに追加)
    update(cache, { data: { createUser: newUser } }) {
      const existing = cache.readQuery({ query: GET_USERS });
      cache.writeQuery({
        query: GET_USERS,
        data: {
          users: [...existing.users, newUser],
        },
      });
    },
    // 成功時のコールバック
    onCompleted(data) {
      console.log("作成成功:", data.createUser);
      setName("");
      setEmail("");
    },
    // エラー時のコールバック
    onError(error) {
      console.error("作成失敗:", error.message);
    },
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await createUser({
      variables: {
        input: { name, email },
      },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="名前"
      />
      <input
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="メールアドレス"
      />
      <button type="submit" disabled={loading}>
        {loading ? "作成中..." : "ユーザーを作成"}
      </button>
      {error && <p>エラー: {error.message}</p>}
    </form>
  );
}

キャッシュ管理

Apollo ClientはInMemoryCacheで 取得したデータを正規化してキャッシュします。 キャッシュを適切に管理することで、パフォーマンスとUXが向上します。

// キャッシュの設定
const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        // ページネーション付きのキャッシュマージ
        posts: {
          keyArgs: ["status"],  // statusごとに別キャッシュ
          merge(existing = [], incoming) {
            return [...existing, ...incoming];
          },
        },
      },
    },
    User: {
      // カスタムキャッシュID
      keyFields: ["email"],  // デフォルトはid
    },
  },
});

// fetchPolicy の種類
const { data } = useQuery(GET_USERS, {
  // "cache-first"      キャッシュ優先(デフォルト)
  // "cache-and-network" キャッシュ返却後、ネットワークも実行
  // "network-only"      常にネットワークから取得
  // "cache-only"        キャッシュのみ
  // "no-cache"          キャッシュを使わない
  fetchPolicy: "cache-and-network",
});

// キャッシュの直接操作
// 読み取り
const cachedData = client.readQuery({
  query: GET_USER,
  variables: { id: "1" },
});

// 書き込み
client.writeQuery({
  query: GET_USER,
  variables: { id: "1" },
  data: {
    user: { ...cachedData.user, name: "更新された名前" },
  },
});

// キャッシュのクリア
await client.resetStore();      // キャッシュクリア + アクティブクエリ再実行
await client.clearStore();      // キャッシュクリアのみ

Optimistic Updates

Optimistic Updatesは、サーバーの応答を待たずに UIを即座に更新する手法です。サーバーからの応答が届いたら、実際のデータで上書きします。

const UPDATE_POST = gql`
  mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
    updatePost(id: $id, input: $input) {
      id
      title
      body
      updatedAt
    }
  }
`;

function PostEditor({ post }) {
  const [updatePost] = useMutation(UPDATE_POST, {
    // Optimistic Response: サーバー応答前にUIを更新
    optimisticResponse: {
      updatePost: {
        __typename: "Post",
        id: post.id,
        title: newTitle,          // 楽観的な値
        body: newBody,
        updatedAt: new Date().toISOString(),
      },
    },
    // update でキャッシュを直接操作
    update(cache, { data: { updatePost } }) {
      // キャッシュ内の該当データが自動的に更新される
      // (idフィールドが一致するため)
      console.log("キャッシュ更新:", updatePost.title);
    },
  });

  const handleSave = () => {
    updatePost({
      variables: {
        id: post.id,
        input: { title: newTitle, body: newBody },
      },
    });
    // → UIは即座に更新される
    // → サーバー応答後、実際のデータで上書き
    // → エラーの場合、楽観的更新がロールバックされる
  };
}

Optimistic Updatesにより、ユーザーは待ち時間なくUIの変化を確認できます。 サーバーエラー時は自動的にロールバックされるので安全です。

エラーハンドリング

Apollo Clientでは、ネットワークエラーとGraphQLエラーの2種類を適切に処理する必要があります。

import { ApolloClient, from, HttpLink } from "@apollo/client";
import { onError } from "@apollo/client/link/error";

// エラーリンクの設定
const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path, extensions }) => {
      console.error(
        `[GraphQL Error]: ${message}`,
        `Path: ${path}`,
        `Code: ${extensions?.code}`
      );

      // 認証エラーの場合、ログインページへリダイレクト
      if (extensions?.code === "UNAUTHENTICATED") {
        window.location.href = "/login";
      }
    });
  }

  if (networkError) {
    console.error(`[Network Error]: ${networkError.message}`);
  }
});

// リンクチェーンの構成
const client = new ApolloClient({
  link: from([
    errorLink,
    new HttpLink({ uri: "http://localhost:4000/graphql" }),
  ]),
  cache: new InMemoryCache(),
});

// コンポーネントでのエラーハンドリング
function UserProfile({ userId }) {
  const { data, loading, error } = useQuery(GET_USER, {
    variables: { id: userId },
    errorPolicy: "all",  // エラーがあってもデータを返す
  });

  if (loading) return <Spinner />;

  if (error) {
    // GraphQLエラーの詳細を表示
    return (
      <div>
        <h2>エラーが発生しました</h2>
        {error.graphQLErrors.map((err, i) => (
          <p key={i}>{err.message}</p>
        ))}
        {error.networkError && (
          <p>ネットワークエラー: {error.networkError.message}</p>
        )}
        <button onClick={() => refetch()}>再試行</button>
      </div>
    );
  }

  return <div>{data?.user?.name}</div>;
}

まとめ

  • ApolloProviderでアプリ全体をラップしてクライアントを注入
  • useQueryでデータ取得、useMutationでデータ変更
  • InMemoryCacheが自動的にデータを正規化してキャッシュ
  • Optimistic Updatesでサーバー応答前にUIを即座に更新
  • エラーリンクでネットワークエラーとGraphQLエラーを一元管理