5 分で理解する Apollo Client - React 開発者のための GraphQL 入門

React開発者の皆さん、API連携で悩んだことはありませんか?
「複数のエンドポイントを叩く必要がある」「必要のないデータまで取得してしまう」「型安全性が保てない」など、REST APIを使った開発では様々な課題に直面することが多いですね。
そんな課題を一気に解決してくれるのが、GraphQLとApollo Clientの組み合わせです。今回は、React開発者の視点から、5分でApollo Clientの基本を理解できるよう、実践的な内容をお届けします。
背景
REST API の課題とGraphQLの登場
従来のREST APIでは、リソースごとにエンドポイントが分かれているため、複雑なデータを取得する際に複数のリクエストが必要でした。
例えば、ユーザー情報と投稿一覧、そしてコメント数を表示したい場合を考えてみましょう。
typescript// REST APIの場合:複数リクエストが必要
const fetchUserData = async (userId: string) => {
const user = await fetch(`/api/users/${userId}`);
const posts = await fetch(`/api/users/${userId}/posts`);
const comments = await fetch(`/api/posts/${postId}/comments`);
return { user, posts, comments };
};
この方法では、ネットワークリクエストが多くなり、パフォーマンスの問題が発生します。
GraphQLは、このような課題を解決するために2012年にFacebookで開発されました。GraphQLでは、必要なデータを1回のリクエストで取得できるため、効率的なデータ取得が可能です。
graphql# GraphQLの場合:1回のリクエストで完結
query GetUserData($userId: ID!) {
user(id: $userId) {
name
email
posts {
title
content
commentsCount
}
}
}
React開発におけるデータ取得の現状
React開発では、状態管理とAPI連携が開発者の大きな悩みの種となっています。
多くの開発者が以下のような課題を抱えているのではないでしょうか。
typescript// 従来のReact開発でよく見るパターン
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
このコードを見ると、データ取得のたびに同じようなボイラープレートコードを書く必要があることがわかります。また、キャッシュやエラーハンドリング、リアルタイム更新なども自分で実装する必要があります。
課題
REST APIでの複数リクエスト問題
REST APIの最大の問題は、関連するデータを取得するために複数のリクエストが必要になることです。
以下の図で、従来のREST APIでのデータ取得フローを確認してみましょう。
mermaidsequenceDiagram
participant C as React Component
participant A as REST API
participant D as Database
C->>A: GET /api/users/1
A->>D: SELECT * FROM users WHERE id = 1
D-->>A: User data
A-->>C: User response
C->>A: GET /api/users/1/posts
A->>D: SELECT * FROM posts WHERE user_id = 1
D-->>A: Posts data
A-->>C: Posts response
C->>A: GET /api/posts/1/comments
A->>D: SELECT * FROM comments WHERE post_id = 1
D-->>A: Comments data
A-->>C: Comments response
この図からもわかるように、3つの関連するデータを取得するために3回のリクエストが必要です。
データの過剰取得・不足取得
REST APIでは、エンドポイントが返すデータ構造が固定されているため、以下のような問題が発生します。
過剰取得の例
typescript// ユーザー名だけ欲しいのに、すべての情報が返ってくる
interface User {
id: string;
name: string;
email: string;
bio: string;
avatar: string;
createdAt: string;
updatedAt: string;
preferences: object;
// ... 他にも多数のフィールド
}
const response = await fetch('/api/users/1');
const user: User = await response.json();
// 実際に使うのは name だけ
console.log(user.name);
不足取得の例
typescript// 投稿一覧は取得できるが、作成者情報は別途取得が必要
interface Post {
id: string;
title: string;
content: string;
userId: string; // IDだけで、実際のユーザー情報は含まれない
}
const posts: Post[] = await fetch('/api/posts').then(r => r.json());
// 各投稿の作成者情報を取得するために追加のリクエストが必要
const postsWithUsers = await Promise.all(
posts.map(async (post) => {
const user = await fetch(`/api/users/${post.userId}`).then(r => r.json());
return { ...post, user };
})
);
型安全性の欠如
TypeScriptを使用していても、API の response が実際に期待する型と一致するかどうかは実行時までわかりません。
typescriptinterface User {
id: string;
name: string;
email: string;
}
// 型注釈はあるが、実際のレスポンスが型と一致する保証はない
const user: User = await fetch('/api/users/1').then(r => r.json());
// 実行時エラーの可能性
console.log(user.name.toUpperCase()); // name が null の場合エラー
これらの課題により、開発効率の低下やランタイムエラーのリスクが増大してしまいます。
解決策
Apollo Clientとは
Apollo Clientは、GraphQLを使用してデータを管理するための包括的なJavaScriptライブラリです。
Reactアプリケーションにおいて、以下の機能を提供してくれます。
機能 | 説明 | メリット |
---|---|---|
データ取得 | GraphQLクエリの実行 | 1回のリクエストで必要なデータをすべて取得 |
キャッシュ | インメモリキャッシュ | 不要なリクエストを削減し、パフォーマンス向上 |
状態管理 | ローカル状態とリモート状態の統合 | 複雑な状態管理ロジックが不要 |
リアルタイム | Subscriptionサポート | リアルタイムデータ更新 |
エラーハンドリング | 一元的なエラー管理 | 一貫したエラー処理 |
以下の図で、Apollo Clientがアプリケーションでどのような役割を果たすかを確認してみましょう。
mermaidflowchart LR
RC[React Components] -->|GraphQL Query| AC[Apollo Client]
AC -->|HTTP Request| GS[GraphQL Server]
GS -->|Response| AC
AC -->|Cached Data| RC
AC -->|Cache| Cache[(In-Memory Cache)]
AC -->|Local State| LS[Local State Management]
subgraph "Apollo Client Core"
Cache
LS
EH[Error Handling]
RT[Real-time Updates]
end
Apollo Clientを使用することで、複雑なデータ管理ロジックを自分で書く必要がなくなります。
GraphQLクエリの威力
GraphQLクエリを使用することで、必要なデータだけを効率的に取得できます。
graphql# 必要なフィールドだけを指定
query GetUserPosts($userId: ID!) {
user(id: $userId) {
name
avatar
posts {
title
content
createdAt
comments {
count
}
}
}
}
このクエリの特徴は以下の通りです。
- 選択的なデータ取得: 必要なフィールドだけを指定
- ネストした関係: userからpostsまで一度に取得
- 型安全性: スキーマに基づく厳密な型チェック
- 単一エンドポイント: 1つのリクエストですべて完結
React Hooksとの連携
Apollo Clientは、React Hooksと自然に統合されており、直感的なAPIを提供します。
useQueryの基本的な使用方法
typescriptimport { useQuery, gql } from '@apollo/client';
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`;
function UsersList() {
const { loading, error, data } = useQuery(GET_USERS);
if (loading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error.message}</div>;
return (
<ul>
{data.users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
このコードを見ると、従来のfetch APIと比較して以下の点で優れていることがわかります。
- ボイラープレートの削減: loading、error、dataが自動で管理される
- 自動リフェッチ: コンポーネントがマウントされると自動でデータを取得
- キャッシュ: 同じクエリは自動的にキャッシュされる
- 型安全性: TypeScriptとの完全な統合
具体例
セットアップから実装まで
まずは、React プロジェクトにApollo Clientをセットアップしましょう。
パッケージのインストール
bashyarn add @apollo/client graphql
Apollo Clientの初期設定
typescript// src/apollo/client.ts
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
// GraphQLサーバーへの接続を設定
const httpLink = createHttpLink({
uri: 'http://localhost:4000/graphql', // GraphQLサーバーのURL
});
// Apollo Clientのインスタンスを作成
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(), // インメモリキャッシュを設定
defaultOptions: {
watchQuery: {
errorPolicy: 'all', // エラーが発生してもキャッシュデータを返す
},
},
});
export default client;
Providerの設定
typescript// src/App.tsx
import React from 'react';
import { ApolloProvider } from '@apollo/client';
import client from './apollo/client';
import UsersList from './components/UsersList';
function App() {
return (
<ApolloProvider client={client}>
<div className="App">
<h1>Apollo Client サンプル</h1>
<UsersList />
</div>
</ApolloProvider>
);
}
export default App;
ApolloProviderによって、アプリケーション全体でApollo Clientを使用できるようになります。
useQueryで始める簡単データ取得
実際にデータを取得するコンポーネントを作成してみましょう。
GraphQLクエリの定義
typescript// src/graphql/queries.ts
import { gql } from '@apollo/client';
export const GET_USERS = gql`
query GetUsers {
users {
id
name
email
avatar
postsCount
}
}
`;
export const GET_USER_DETAIL = gql`
query GetUserDetail($userId: ID!) {
user(id: $userId) {
id
name
email
bio
avatar
posts {
id
title
content
createdAt
commentsCount
}
}
}
`;
TypeScriptの型定義
typescript// src/types/user.ts
export interface User {
id: string;
name: string;
email: string;
avatar?: string;
postsCount: number;
}
export interface Post {
id: string;
title: string;
content: string;
createdAt: string;
commentsCount: number;
}
export interface UserDetail extends User {
bio?: string;
posts: Post[];
}
ユーザー一覧コンポーネント
typescript// src/components/UsersList.tsx
import React from 'react';
import { useQuery } from '@apollo/client';
import { GET_USERS } from '../graphql/queries';
import { User } from '../types/user';
interface UsersData {
users: User[];
}
function UsersList() {
const { loading, error, data, refetch } = useQuery<UsersData>(GET_USERS, {
pollInterval: 30000, // 30秒ごとに自動更新
});
if (loading) return <div className="loading">データを読み込み中...</div>;
if (error) {
return (
<div className="error">
<p>エラーが発生しました: {error.message}</p>
<button onClick={() => refetch()}>
再試行
</button>
</div>
);
}
return (
<div className="users-list">
<h2>ユーザー一覧</h2>
<button onClick={() => refetch()}>
最新データを取得
</button>
<div className="users-grid">
{data?.users.map(user => (
<div key={user.id} className="user-card">
<img src={user.avatar || '/default-avatar.png'} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.email}</p>
<span>投稿数: {user.postsCount}</span>
</div>
))}
</div>
</div>
);
}
export default UsersList;
このコンポーネントでは、以下の機能が自動的に提供されます。
- 自動ローディング状態: loading フラグでUIを制御
- エラーハンドリング: error オブジェクトで一元管理
- 自動更新: pollInterval で定期的なデータ更新
- 手動リフェッチ: refetch 関数でオンデマンド更新
useMutationでデータ更新
次に、データの作成・更新・削除を行うミューテーションを実装してみましょう。
ミューテーションの定義
typescript// src/graphql/mutations.ts
import { gql } from '@apollo/client';
export const CREATE_USER = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
avatar
}
}
`;
export const UPDATE_USER = gql`
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
bio
avatar
}
}
`;
export const DELETE_USER = gql`
mutation DeleteUser($id: ID!) {
deleteUser(id: $id) {
success
message
}
}
`;
ユーザー作成フォーム
typescript// src/components/CreateUserForm.tsx
import React, { useState } from 'react';
import { useMutation } from '@apollo/client';
import { CREATE_USER, GET_USERS } from '../graphql/queries';
interface CreateUserInput {
name: string;
email: string;
bio?: string;
}
function CreateUserForm() {
const [formData, setFormData] = useState<CreateUserInput>({
name: '',
email: '',
bio: '',
});
const [createUser, { loading, error }] = useMutation(CREATE_USER, {
// ミューテーション完了後にユーザー一覧を再取得
refetchQueries: [{ query: GET_USERS }],
onCompleted: (data) => {
console.log('ユーザーが作成されました:', data.createUser);
// フォームをリセット
setFormData({ name: '', email: '', bio: '' });
},
onError: (error) => {
console.error('ユーザー作成エラー:', error);
},
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await createUser({
variables: {
input: formData,
},
});
} catch (err) {
// エラーは onError で処理される
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value,
}));
};
return (
<form onSubmit={handleSubmit} className="create-user-form">
<h2>新しいユーザーを作成</h2>
{error && (
<div className="error-message">
エラー: {error.message}
</div>
)}
<div className="form-group">
<label htmlFor="name">名前</label>
<input
id="name"
name="name"
type="text"
value={formData.name}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="email">メールアドレス</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="bio">自己紹介</label>
<textarea
id="bio"
name="bio"
value={formData.bio}
onChange={handleChange}
rows={3}
/>
</div>
<button
type="submit"
disabled={loading}
className="submit-button"
>
{loading ? '作成中...' : 'ユーザーを作成'}
</button>
</form>
);
}
export default CreateUserForm;
キャッシュ更新の最適化
より効率的なキャッシュ更新のため、update
関数を使用することもできます。
typescript// より効率的なキャッシュ更新
const [createUser] = useMutation(CREATE_USER, {
update: (cache, { data }) => {
// 既存のキャッシュデータを読み込み
const existingUsers = cache.readQuery<{ users: User[] }>({
query: GET_USERS,
});
if (existingUsers && data?.createUser) {
// 新しいユーザーを既存のリストに追加
cache.writeQuery({
query: GET_USERS,
data: {
users: [...existingUsers.users, data.createUser],
},
});
}
},
});
以下の図で、ミューテーション実行時のキャッシュ更新フローを確認してみましょう。
mermaidflowchart TD
A[useMutation実行] --> B[GraphQLサーバーにリクエスト]
B --> C[レスポンス受信]
C --> D{update関数は定義済み?}
D -->|Yes| E[update関数でキャッシュを手動更新]
D -->|No| F[refetchQueriesで関連クエリを再実行]
E --> G[UIが自動更新]
F --> G
G --> H[完了コールバック実行]
まとめ
Apollo Clientを使用することで、React開発における多くの課題を解決できることがわかりました。
主なメリットをまとめると以下のようになります。
従来の方法 | Apollo Client |
---|---|
複数のREST APIリクエスト | 1つのGraphQLクエリで完結 |
手動でのローディング状態管理 | useQueryで自動管理 |
複雑なキャッシュロジック | 自動キャッシュとインテリジェントな更新 |
型安全性の不足 | GraphQLスキーマベースの完全な型サポート |
散在するエラーハンドリング | 一元的なエラー管理 |
開発効率の向上という観点では、以下の点で大きな改善が期待できます。
- ボイラープレートコードの大幅削減
- 自動的なキャッシュ管理によるパフォーマンス最適化
- リアルタイム機能の簡単な実装
- 一貫性のあるデータ管理パターン
学習コストについてですが、GraphQLの基本概念を理解する必要はありますが、Apollo Clientが提供するReact Hooksは直感的で、既存のReact知識があればすぐに使い始めることができます。
今回ご紹介した内容は、Apollo Clientの基本的な機能のほんの一部です。実際のプロジェクトでは、Subscriptionによるリアルタイム更新、楽観的UI更新、オフライン対応など、さらに高度な機能も活用できます。
ぜひ一度、実際のプロジェクトでApollo Clientを試してみて、その開発体験の向上を実感してみてください。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来