はじめに
GraphQLを使い始めると、すぐに壁にぶつかります。loading・error・dataの分岐を毎回自前で書き、取得したデータをどこに置くか、同じクエリを別の画面でも使うときにどう再利用するかを考え、キャッシュの更新漏れでUIだけ古いままになる……。REST時代のfetchをGraphQLに置き換えただけでは、こうした面倒はむしろ増えてしまいます。
Apollo Clientは、この「取得・キャッシュ・更新」をまとめて引き受けてくれるGraphQLクライアントです。useQueryやuseMutationといったReactフックを使うだけで、ローディング状態の管理も、正規化されたキャッシュへの反映も、ライブラリ側が面倒を見てくれます。
とはいえ、読むより触った方が早いと思います。タスク一覧をボタン一つで切り替えたり、わざとエラーを起こしたりできるサンプルを用意したので、先に挙動を見たい方はこちらからどうぞ。
Apollo Clientとは
Apollo Clientは、JavaScript/TypeScript向けの包括的なGraphQL状態管理ライブラリです。React・Vue・Angularなど主要なフレームワークに対応しており、GraphQLサーバーへの問い合わせ結果を正規化キャッシュで管理しながら、UIの再レンダリングまで自動で行います。単なる「fetchのラッパー」ではなく、アプリケーション全体のデータレイヤーとして設計されている点が特徴です。
主な特徴
- 宣言的なデータ取得 -
useQueryにクエリを渡すだけで、読み込み中・成功・失敗の状態が自動的に返ってくる - 正規化キャッシング - 取得したデータを
id単位で正規化して保存し、同じエンティティを参照する別のクエリにも即座に反映する - TypeScriptファースト - クエリ結果の型付けやコード生成との連携が前提で設計されている
- モダンReact対応 -
useSuspenseQueryによるSuspense連携やReact Compilerとの親和性まで見据えたAPI設計
インストール
Apollo Client v4では、GraphQL本体に加えてrxjsが必須の依存関係になりました。以下の3つを合わせてインストールします。
npm install @apollo/client graphql rxjs
Yarnを使う場合は次のようになります。
yarn add @apollo/client graphql rxjs
Apollo Clientのサンプルを動かす
下のサンプルは、useQueryでタスク一覧を取得するReactアプリです。実際のサーバーの代わりに、Apollo Client公式のテストユーティリティMockedProvider(@apollo/client/testing/react)でGraphQLレスポンスを再現しているので、外部通信なしでその場で挙動を確認できます。ボタンでstatus変数を切り替えるとuseQueryが再実行され、一覧が入れ替わります。
まずは中心となる部分だけを抜き出したコードです。useQueryの第2引数にvariablesを渡すと、その値が変わるたびに自動で再取得が走ります。
const GET_TASKS = gql`
query GetTasks($status: String) {
tasks(status: $status) { id title done }
}
`
function TaskList({ status }: { status: string }) {
const { loading, error, data } = useQuery(GET_TASKS, {
variables: { status },
})
if (loading) return <p>読み込み中...</p>
if (error) return <p>エラー: {error.message}</p>
return (
<ul>
{data.tasks.map((t) => <li key={t.id}>{t.title}</li>)}
</ul>
)
}
実際に動かせるものが下です。「すべて」「未完了」「完了」を切り替えてキャッシュの挙動を確認したあと、「わざとエラーを起こす」を押してみてください。モックに用意していないstatusを渡した扱いになり、useQueryが返すerrorに「対応するモックが見つからない」旨のエラーが入ることが確認できます。実際のアプリでサーバーエラーが起きたときも、同じようにerrorステートで検知できます。
ポイントは、statusという同じ変数名でもクエリが異なれば別のキャッシュエントリとして扱われることです。「すべて」→「未完了」→「すべて」と切り替えても、Apollo Clientの正規化キャッシュのおかげでボタンを押すたびに一覧がちらつかず切り替わります。
基本的な使い方
実際のGraphQLサーバーに接続するときは、ApolloClientインスタンスを作り、ApolloProviderでReactツリーをラップします。
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
import { ApolloProvider, useQuery, gql } from "@apollo/client/react";
const client = new ApolloClient({
link: new HttpLink({ uri: "https://example.com/graphql" }),
cache: new InMemoryCache(),
});
const GET_LOCATIONS = gql`
query GetLocations {
locations {
id
name
}
}
`;
function DisplayLocations() {
const { loading, error, data } = useQuery(GET_LOCATIONS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return data.locations.map(({ id, name }) => <div key={id}>{name}</div>);
}
function App() {
return (
<ApolloProvider client={client}>
<DisplayLocations />
</ApolloProvider>
);
}
v4ではApolloClientやInMemoryCacheのような通信・キャッシュ層は@apollo/client本体から、ApolloProviderやuseQueryのようなReact連携は@apollo/client/reactからインポートする構成に整理されています。パッケージを分けて考えると、Apollo Clientの「通信・キャッシュを担うコア」と「Reactへのバインディング」の役割の違いが分かりやすくなります。
実践的なユースケース
useMutationでのデータ更新とキャッシュ反映
一覧取得だけでなく、useMutationを使ったデータ更新もApollo Clientの中心的な機能です。optimisticResponseを渡すと、サーバーからの応答を待たずに画面を即座に更新し、実際のレスポンスが届いた時点で正しい値に差し替えられます。
const [likePost, { data, loading, error }] = useMutation(LIKE_POST, {
variables: { postId: 'p1' },
optimisticResponse: {
likePost: { __typename: 'Post', id: 'p1', likes: 12 },
},
})
return (
<button onClick={() => likePost()} disabled={loading}>
❤️ いいね
</button>
)
下のサンプルでは「いいね」ボタンを押すたびにuseMutationが実行されます。モックは3回分しか用意していないので、4回目を押すとどうなるか試してみてください。errorに「モックが尽きた」旨のメッセージが入り、実運用でサーバーエラーが起きたときと同じようにerrorステートで検知できることが分かります。
useSuspenseQueryとReact Suspenseの連携
Apollo Client v4はReact 19のSuspenseと組み合わせられるuseSuspenseQueryを提供しています。useQueryと違いloadingという真偽値を自分で扱う必要がなく、データ取得中は親のSuspense境界がfallbackを表示してくれます。
function Profile() {
const { data } = useSuspenseQuery(GET_PROFILE)
return <p>{data.profile.name}さん</p>
}
<Suspense fallback={<p>読み込み中...</p>}>
<Profile />
</Suspense>
下のサンプルは、あえて1.2秒の遅延を入れたモックでuseSuspenseQueryを実行しています。Profileコンポーネント自体にはloadingの分岐が一切登場せず、ローディング表示は外側のSuspenseのfallbackだけが担当している点に注目してください。
useQueryが「コンポーネント内でローディング状態を扱う」スタイルだとすると、useSuspenseQueryは「ローディング状態を親に委譲する」スタイルです。一覧ページのスケルトン表示など、複数のコンポーネントのローディングをまとめて扱いたい場面で特に効果を発揮します。
まとめ
Apollo Clientを使うと、GraphQLのデータ取得にまつわる「ローディング管理」「キャッシュの整合性」「更新後の反映漏れ」といった面倒を、useQuery・useMutation・useSuspenseQueryといったフックに任せられます。今回のサンプルではMockedProviderを使いましたが、実際のプロジェクトではHttpLinkで本物のGraphQLエンドポイントに接続するだけで同じコードがそのまま動きます。まずは手元の小さな画面から、useQuery一つ導入してみるところから始めてみてはいかがでしょうか。