apollo-graphql について
apollo-graphqlは、Apollo Client、TypeScript、Reactを使ったGraphQLアプリ開発の実践指針です。クエリとミューテーション、キャッシュ、ページネーション、エラー処理、クライアント側の状態管理を扱います。
主な機能
- 01Apollo Clientの設定とフック
- 02再利用可能なクエリフラグメント
- 03型安全なGraphQL操作
- 04キャッシュポリシーとページネーション
- 05バッチ処理とDataLoaderの指針
- 06クエリとミューテーションのエラー処理
活用シーン
- 01GraphQLクエリとミューテーションの整理
- 02型安全なReactデータフックの構築
- 03カーソルページネーションによる大規模データ処理
- 04バックエンドへの重複リクエスト削減
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ mindrally/ skills --skill apollo-graphqlインストール後、Claude Code で/apollo-graphqlを入力して読み込まれたか確認します。
詳細
Apollo GraphQL のベストプラクティス
あなたは Apollo Client、GraphQL、TypeScript、React 開発のエキスパートです。Apollo Client は、インテリジェントなキャッシュ、楽観的 UI 更新、シームレスな React 連携を備えた、GraphQL アプリケーション向けの包括的な状態管理ソリューションを提供します。
基本原則
- Apollo Client を状態管理とデータ取得に使用します
- データ取得にはクエリーコンポーネントを実装します
- データの変更にはミューテーションを使用します
- 再利用可能なクエリーの部分にはフラグメントを使用します
- 適切なエラー処理と読み込み状態を実装します
- GraphQL オペレーションの型安全性には TypeScript を活用します
プロジェクト構成
src/
components/
graphql/
queries/
users.ts
posts.ts
mutations/
users.ts
posts.ts
fragments/
user.ts
post.ts
hooks/
useUser.ts
usePosts.ts
pages/
utils/
apollo-client.ts
types/
generated/ # Generated TypeScript types
セットアップと設定
Apollo Client のセットアップ
// utils/apollo-client.ts
import { ApolloClient, InMemoryCache, HttpLink, from } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
const httpLink = new HttpLink({
uri: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT,
});
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) => {
console.error(`[GraphQL error]: Message: ${message}, Path: ${path}`);
});
}
if (networkError) {
console.error(`[Network error]: ${networkError}`);
}
});
export const apolloClient = new ApolloClient({
link: from([errorLink, httpLink]),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
users: {
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
},
}),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
errorPolicy: 'all',
},
query: {
fetchPolicy: 'cache-first',
errorPolicy: 'all',
},
mutate: {
errorPolicy: 'all',
},
},
});
Apollo Provider のセットアップ
// pages/_app.tsx or app/providers.tsx
import { ApolloProvider } from '@apollo/client';
import { apolloClient } from '@/utils/apollo-client';
function App({ children }: { children: React.ReactNode }) {
return (
<ApolloProvider client={apolloClient}>
{children}
</ApolloProvider>
);
}
スキーマ設計のベストプラクティス
命名規則
型、フィールド、引数には内容が分かりやすい名前を使用します。
# Good
type User {
id: ID!
firstName: String!
lastName: String!
emailAddress: String!
createdAt: DateTime!
}
type Query {
getUserById(id: ID!): User
getUsersByRole(role: UserRole!): [User!]!
}
# Avoid
type Query {
getUser(id: ID!): User # Less descriptive
}
スキーマ構造
ビジネスドメインを反映した、明確なスキーマを定義します。
type Query {
user(id: ID!): User
users(first: Int, after: String, filter: UserFilter): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
deleteUser(id: ID!): DeleteUserPayload!
}
input CreateUserInput {
firstName: String!
lastName: String!
email: String!
}
type CreateUserPayload {
user: User
errors: [UserError!]
}
クエリーパターン
フラグメントを使ったクエリーの定義
// graphql/fragments/user.ts
import { gql } from '@apollo/client';
export const USER_FIELDS = gql`
fragment UserFields on User {
id
firstName
lastName
email
avatar
createdAt
}
`;
// graphql/queries/users.ts
import { gql } from '@apollo/client';
import { USER_FIELDS } from '../fragments/user';
export const GET_USER = gql`
${USER_FIELDS}
query GetUser($id: ID!) {
user(id: $id) {
...UserFields
}
}
`;
export const GET_USERS = gql`
${USER_FIELDS}
query GetUsers($first: Int, $after: String) {
users(first: $first, after: $after) {
edges {
node {
...UserFields
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
カスタムクエリーフック
// hooks/useUser.ts
import { useQuery, QueryHookOptions } from '@apollo/client';
import { GET_USER } from '@/graphql/queries/users';
import { User, GetUserQuery, GetUserQueryVariables } from '@/types/generated';
export function useUser(
id: string,
options?: QueryHookOptions<GetUserQuery, GetUserQueryVariables>
) {
const { data, loading, error, refetch } = useQuery<
GetUserQuery,
GetUserQueryVariables
>(GET_USER, {
variables: { id },
skip: !id,
...options,
});
return {
user: data?.user,
loading,
error,
refetch,
};
}
ミューテーションパターン
ミューテーションの定義
// graphql/mutations/users.ts
import { gql } from '@apollo/client';
import { USER_FIELDS } from '../fragments/user';
export const CREATE_USER = gql`
${USER_FIELDS}
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
user {
...UserFields
}
errors {
field
message
}
}
}
`;
export const UPDATE_USER = gql`
${USER_FIELDS}
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
user {
...UserFields
}
errors {
field
message
}
}
}
`;
カスタムミューテーションフック
// hooks/useCreateUser.ts
import { useMutation, MutationHookOptions } from '@apollo/client';
import { CREATE_USER } from '@/graphql/mutations/users';
import { GET_USERS } from '@/graphql/queries/users';
export function useCreateUser(options?: MutationHookOptions) {
const [createUser, { data, loading, error }] = useMutation(CREATE_USER, {
refetchQueries: [{ query: GET_USERS }],
onError: (error) => {
console.error('Failed to create user:', error);
},
...options,
});
return {
createUser: (input: CreateUserInput) => createUser({ variables: { input } }),
data,
loading,
error,
};
}
楽観的更新
function useUpdateUser() {
const [updateUser] = useMutation(UPDATE_USER, {
optimisticResponse: ({ id, input }) => ({
__typename: 'Mutation',
updateUser: {
__typename: 'UpdateUserPayload',
user: {
__typename: 'User',
id,
...input,
},
errors: null,
},
}),
update: (cache, { data }) => {
const updatedUser = data?.updateUser?.user;
if (updatedUser) {
cache.modify({
id: cache.identify(updatedUser),
fields: {
firstName: () => updatedUser.firstName,
lastName: () => updatedUser.lastName,
},
});
}
},
});
return { updateUser };
}
キャッシュ戦略
キャッシュの正規化
const cache = new InMemoryCache({
typePolicies: {
User: {
keyFields: ['id'],
},
Post: {
keyFields: ['id'],
fields: {
author: {
merge: true,
},
},
},
},
});
キャッシュの読み取りと書き込み
// Read from cache
const user = client.readFragment({
id: `User:${userId}`,
fragment: USER_FIELDS,
});
// Write to cache
client.writeFragment({
id: `User:${userId}`,
fragment: USER_FIELDS,
data: {
...user,
firstName: 'Updated Name',
},
});
ページネーション
カーソルベースのページネーション(Relay スタイル)
大量のデータや頻繁に変化するデータには、カーソルベースのページネーションを推奨します。
function useInfiniteUsers() {
const { data, loading, fetchMore } = useQuery(GET_USERS, {
variables: { first: 10 },
});
const loadMore = () => {
if (!data?.users.pageInfo.hasNextPage) return;
fetchMore({
variables: {
after: data.users.pageInfo.endCursor,
},
});
};
return {
users: data?.users.edges.map((edge) => edge.node) ?? [],
loading,
hasMore: data?.users.pageInfo.hasNextPage ?? false,
loadMore,
};
}
ページネーション向けのキャッシュマージポリシー
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
users: {
keyArgs: ['filter'],
merge(existing = { edges: [] }, incoming) {
return {
...incoming,
edges: [...existing.edges, ...incoming.edges],
};
},
},
},
},
},
});
パフォーマンスの最適化
DataLoader パターン
バックエンドへのリクエストを減らすために、バッチ処理の手法を使用します。
// Server-side with DataLoader
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (ids: string[]) => {
const users = await db.users.findMany({ where: { id: { in: ids } } });
return ids.map((id) => users.find((u) => u.id === id));
});
// In resolver
const resolvers = {
Post: {
author: (post) => userLoader.load(post.authorId),
},
};
クエリーのバッチ処理
import { BatchHttpLink } from '@apollo/client/link/batch-http';
const batchLink = new BatchHttpLink({
uri: '/graphql',
batchMax: 10,
batchInterval: 20,
});
フェッチポリシー
// Network only - skip cache
useQuery(GET_USER, {
fetchPolicy: 'network-only',
});
// Cache first - prefer cache
useQuery(GET_USER, {
fetchPolicy: 'cache-first',
});
// Cache and network - return cache, then update
useQuery(GET_USER, {
fetchPolicy: 'cache-and-network',
});
エラー処理
クエリーのエラー処理
function UserProfile({ userId }: { userId: string }) {
const { data, loading, error } = useUser(userId);
if (loading) return <Skeleton />;
if (error) {
return (
<ErrorMessage
message="Failed to load user profile"
retry={() => refetch()}
/>
);
}
return <ProfileCard user={data} />;
}
ミューテーションのエラー処理
function CreateUserForm() {
const { createUser, loading, error } = useCreateUser({
onCompleted: (data) => {
if (data.createUser.errors?.length) {
// Handle validation errors
data.createUser.errors.forEach((err) => {
setFieldError(err.field, err.message);
});
} else {
// Success
toast.success('User created successfully');
}
},
});
// ...
}
状態管理
シンプルな状態管理には、Apollo Client のローカル状態管理を使用します。
// Define local-only fields
const typeDefs = gql`
extend type Query {
isLoggedIn: Boolean!
cartItems: [CartItem!]!
}
`;
// Read local state
const IS_LOGGED_IN = gql`
query IsLoggedIn {
isLoggedIn @client
}
`;
// Write local state
client.writeQuery({
query: IS_LOGGED_IN,
data: { isLoggedIn: true },
});
複雑なクライアント側の状態には、Apollo と併せて Zustand または Redux Toolkit の使用を検討してください。
避けるべきアンチパターン
- 過剰取得/取得不足:必要なフィールドのみをリクエストします
- おしゃべりな API:バッチ処理と DataLoader を使って往復回数を最小限に抑えます
- 神オブジェクト:フィールドが多すぎる大規模でモノリシックな型は避けます
- エラー処理の欠如:クエリーとミューテーションの両方で必ずエラーを処理します
- キャッシュの無視:パフォーマンス向上のために Apollo のキャッシュを活用します
- フラグメントの未使用:フラグメントを使うと、再利用性と保守性が向上します
- TypeScript の省略:型安全性を確保するため、スキーマから型を生成します
主な規約
- アプリケーションのルートで Apollo Provider を使用します
- Apollo オペレーション用のカスタムフックを実装します
- GraphQL オペレーションの型安全性には TypeScript を使用します(型を生成します)
- クエリー、ミューテーション、フラグメントは別々のファイルに整理します
- 再利用可能なクエリーの部分にはフラグメントを使用します
- 適切なエラー処理と読み込み状態を実装します
- 大規模なデータセットにはカーソルベースのページネーションを使用します
- 効率的なデータ読み込みには DataLoader を活用します
コメント
Markdown 対応