mcpField
A
apollo-graphql

apollo-graphql

github.com/mindrally開発287681 回インストール3 週間前に更新
リポジトリ

apollo-graphql について

apollo-graphqlは、Apollo Client、TypeScript、Reactを使ったGraphQLアプリ開発の実践指針です。クエリとミューテーション、キャッシュ、ページネーション、エラー処理、クライアント側の状態管理を扱います。

主な機能

  1. 01Apollo Clientの設定とフック
  2. 02再利用可能なクエリフラグメント
  3. 03型安全なGraphQL操作
  4. 04キャッシュポリシーとページネーション
  5. 05バッチ処理とDataLoaderの指針
  6. 06クエリとミューテーションのエラー処理

活用シーン

  1. 01GraphQLクエリとミューテーションの整理
  2. 02型安全なReactデータフックの構築
  3. 03カーソルページネーションによる大規模データ処理
  4. 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 の省略:型安全性を確保するため、スキーマから型を生成します

主な規約

  1. アプリケーションのルートで Apollo Provider を使用します
  2. Apollo オペレーション用のカスタムフックを実装します
  3. GraphQL オペレーションの型安全性には TypeScript を使用します(型を生成します)
  4. クエリー、ミューテーション、フラグメントは別々のファイルに整理します
  5. 再利用可能なクエリーの部分にはフラグメントを使用します
  6. 適切なエラー処理と読み込み状態を実装します
  7. 大規模なデータセットにはカーソルベースのページネーションを使用します
  8. 効率的なデータ読み込みには DataLoader を活用します

コメント