apollo-graphql
Apollo ClientをState管理・データ取得・キャッシュに活用したGraphQL APIおよびReactアプリケーションの開発ガイドラインです。GraphQL APIの設計からApollo Clientの導入・運用まで、一貫したベストプラクティスを提供します。
description の原文を見る
Guidelines for developing GraphQL APIs and React applications using Apollo Client for state management, data fetching, and caching
SKILL.md 本文
Apollo GraphQL ベストプラクティス
Apollo Client、GraphQL、TypeScript、React 開発の専門家です。Apollo Client は、GraphQL アプリケーション向けの包括的な状態管理ソリューションを提供し、インテリジェントなキャッシング、楽観的 UI 更新、シームレスな React 統合を実現します。
コア原則
- 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,
},
},
},
},
});
キャッシュの読み取りと書き込み
// キャッシュから読み取り
const user = client.readFragment({
id: `User:${userId}`,
fragment: USER_FIELDS,
});
// キャッシュに書き込み
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,
});
フェッチポリシー
// ネットワークのみ - キャッシュをスキップ
useQuery(GET_USER, {
fetchPolicy: 'network-only',
});
// キャッシュを優先 - キャッシュを優先
useQuery(GET_USER, {
fetchPolicy: 'cache-first',
});
// キャッシュとネットワーク - キャッシュを返し、その後更新
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) {
// 検証エラーを処理
data.createUser.errors.forEach((err) => {
setFieldError(err.field, err.message);
});
} else {
// 成功
toast.success('User created successfully');
}
},
});
// ...
}
状態管理
シンプルな状態要件には、Apollo Client のローカル状態管理を使用します:
// ローカルのみのフィールドを定義
const typeDefs = gql`
extend type Query {
isLoggedIn: Boolean!
cartItems: [CartItem!]!
}
`;
// ローカル状態を読み取り
const IS_LOGGED_IN = gql`
query IsLoggedIn {
isLoggedIn @client
}
`;
// ローカル状態に書き込み
client.writeQuery({
query: IS_LOGGED_IN,
data: { isLoggedIn: true },
});
複雑なクライアント側の状態には、Apollo と並行して Zustand または Redux Toolkit の使用を検討してください。
避けるべきアンチパターン
- 過度な取得/不足した取得: 必要なフィールドのみをリクエスト
- チャットな API: バッチング と DataLoader でラウンドトリップを最小化
- God Objects: フィールドが多すぎる大型モノリシック型を避ける
- エラーハンドリングの欠落: クエリとミューテーションレベルでエラーを常に処理
- キャッシュの無視: パフォーマンスのため Apollo のキャッシングを活用
- フラグメントを使用しない: フラグメントは再利用性と保守性を改善
- TypeScript をスキップ: スキーマから型を生成して型安全性を確保
主な規約
- アプリケーションのルートで Apollo Provider を使用
- Apollo 操作用のカスタムフックを実装
- GraphQL 操作の型安全性のため TypeScript を使用 (型を生成)
- クエリ、ミューテーション、フラグメントを個別のファイルに整理
- 再利用可能なクエリ部分にフラグメントを使用
- 適切なエラーハンドリングとローディング状態を実装
- 大規模データセットではカーソルベースのページネーションを使用
- 効率的なデータ読み込みのため DataLoader を活用
ライセンス: Apache-2.0(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- mindrally
- リポジトリ
- mindrally/skills
- ライセンス
- Apache-2.0
- 最終更新
- 不明
Source: https://github.com/mindrally/skills / ライセンス: Apache-2.0
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。