Apollo Clientのセットアップ
このページでは、Apollo ClientをAppSyncエンドポイントで動作させるために必要なすべてのことについて説明します。前提条件、Apollo Clientのインストール、GraphQL操作の記述、_versionメタデータの理解、認証とエラーハンドリング用のリンクチェーンの構成、およびAmplifyでのリアルタイムサブスクリプションのセットアップについて説明しています。
開始前に
開始する前に、次のことを確認してください:
- デプロイされて動作している既存のAmplify Gen 1バックエンドとデータモデル
- プロジェクト内のAmplify設定ファイル(
amplifyconfiguration.jsonまたはaws-exports.js) - インストールされて設定済みの
aws-amplifyv6パッケージ(アプリのスタートアップでAmplify.configure(config)が呼び出されている) - GraphQL構文の理解 -- クエリ、ミューテーション、サブスクリプション
Apollo Clientのインストール
Apollo Clientをインストールします:
npm install @apollo/client@^3.14.0graphqlを個別にインストールする必要はありません -- aws-amplifyによって既に提供されています。graphqlを明示的にインストールすると、npmはより新しいバージョン(v16)を解決するため、aws-amplifyの固定されたgraphql@15.8.0と競合し、ERESOLVEエラーで失敗します。
GraphQLエンドポイントを見つける
GraphQLエンドポイントと認証設定はaws-exports.js(またはamplifyconfiguration.json)にあります:
{ "aws_appsync_graphqlEndpoint": "https://xxxxx.appsync-api.us-east-1.amazonaws.com/graphql", "aws_appsync_authenticationType": "AMAZON_COGNITO_USER_POOLS", "aws_appsync_region": "us-east-1"}Apollo Clientを構成するときにaws_appsync_graphqlEndpointの値を使用します。
型付き操作の生成(オプション)
Gen 1プロジェクトには、src/graphql/にすでに自動生成されたGraphQL操作(クエリ、ミューテーション、サブスクリプション)があります。これらの操作はGen 1バックエンドで引き続き機能し、以下のApollo Client操作を記述するときに参照できます。
または、操作を再生成するか、AWS AppSyncコンソールからクエリ、ミューテーション、サブスクリプションを直接コピーできます。Schemaタブとqueriesタブに移動してください。
Apollo Clientでの生成された型の統合の詳細については、高度なパターンページを参照してください。
GraphQL操作を記述する
Apollo Clientはgqlタグ付きテンプレートリテラルを使用してGraphQL操作を定義します。このセクションでは、Postモデルを実行例として、標準的なパターンを示します。
再利用可能なフィールド選択用のGraphQLフラグメント
フラグメントを使用すると、再利用可能なフィールドセットを定義できます。すべての操作がこのフラグメントを参照し、アプリ全体で一貫したフィールド選択を確保します:
fragment PostDetails on Post { id title content status rating createdAt updatedAt _version _deleted _lastChangedAt owner}モデルが所有者ベースの認可(@auth(rules: [{ allow: owner }]))を使用する場合、フラグメントにownerフィールドを含めてください。このフィールドは所有者スコープのサブスクリプションに必要です。
完全な操作定義
import { gql } from '@apollo/client';
// Fragment for consistent field selectionconst POST_DETAILS_FRAGMENT = gql` fragment PostDetails on Post { id title content status rating createdAt updatedAt _version _deleted _lastChangedAt owner }`;
// List all postsexport const LIST_POSTS = gql` ${POST_DETAILS_FRAGMENT} query ListPosts($filter: ModelPostFilterInput, $limit: Int, $nextToken: String) { listPosts(filter: $filter, limit: $limit, nextToken: $nextToken) { items { ...PostDetails } nextToken } }`;
// Get a single post by IDexport const GET_POST = gql` ${POST_DETAILS_FRAGMENT} query GetPost($id: ID!) { getPost(id: $id) { ...PostDetails } }`;
// Create a new postexport const CREATE_POST = gql` ${POST_DETAILS_FRAGMENT} mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { ...PostDetails } }`;
// Update an existing postexport const UPDATE_POST = gql` ${POST_DETAILS_FRAGMENT} mutation UpdatePost($input: UpdatePostInput!) { updatePost(input: $input) { ...PostDetails } }`;
// Delete a postexport const DELETE_POST = gql` ${POST_DETAILS_FRAGMENT} mutation DeletePost($input: DeletePostInput!) { deletePost(input: $input) { ...PostDetails } }`;すべての操作 -- ミューテーションを含む -- は完全なPostDetailsフラグメントを返します。これにより、後続のミューテーション用に常に最新の_version値を持つことが保証されます。
_versionメタデータを理解する
これはこのガイドの最も重要なセクションの1つです。アプリがDataStoreを使用していた場合、バックエンドは競合解決が有効になっており、3つのメタデータフィールドを正しく処理しないとミューテーションが失敗します。
これらのフィールドが存在する理由
DataStoreはAppSyncバックエンド上でDynamoDBを介して競合解決を有効にします。このメカニズムはすべてのモデルに3つのメタデータフィールドを追加します:
| フィールド | 型 | 目的 |
|---|---|---|
_version | Int | オプティミスティックロックカウンター。正常なミューテーションのたびにインクリメントされます。 |
_deleted | Boolean | ソフト削除フラグ。trueの場合、レコードは論理的に削除されますが、DynamoDBに存在します。 |
_lastChangedAt | AWSTimestamp | 最後の変更のミリ秒タイムスタンプ。AppSyncによって自動的に設定されます。 |
それらが必要な場合
- すべてのミューテーションには
_versionが必要です(作成を除く)。省略するとConditionalCheckFailedExceptionが発生します。 - すべてのクエリは
_version、_deleted、_lastChangedAtを応答フィールドで選択する必要があります。 - リストクエリはソフト削除されたレコードを返します。 アプリケーションコードでそれらをフィルタリングする必要があります。
それらを処理する方法
これら3つのルールに従ってください:
1.常にメタデータフィールドを応答選択に含めます。 すべてのクエリとミューテーション応答には、_version、_deleted、_lastChangedAtを含める必要があります(上記のPostDetailsフラグメントはこれを行います)。
2.クエリ結果から常に_versionをミューテーション入力に渡します:
// First, query the current post (includes _version in response)const { data } = await apolloClient.query({ query: GET_POST, variables: { id: postId },});const post = data.getPost;
// Then, pass _version when updatingawait apolloClient.mutate({ mutation: UPDATE_POST, variables: { input: { id: post.id, title: 'Updated Title', _version: post._version, // REQUIRED }, },});3.リストクエリ結果からソフト削除されたレコードをフィルタリングします:
const { data } = await apolloClient.query({ query: LIST_POSTS });const activePosts = data.listPosts.items.filter(post => !post._deleted);ヘルパー:ソフト削除されたレコードをフィルタリング
リストクエリからソフト削除されたレコードをフィルタリングするシンプルなユーティリティ関数:
function filterDeleted<T extends { _deleted?: boolean | null }>(items: T[]): T[] { return items.filter(item => !item._deleted);}
// Usageconst { data } = await apolloClient.query({ query: LIST_POSTS });const activePosts = filterDeleted(data.listPosts.items);Apollo Clientを構成する
Apollo Clientはリンクチェーン -- 各リクエストを処理する一連のミドルウェア関数を介してAppSyncと通信します。4つのリンクを構築します:
- HTTPリンク -- 実際のGraphQLリクエストをAppSyncに送信します
- 認証リンク -- 各リクエストにCognito IDトークンを挿入します
- エラーリンク -- GraphQLおよびネットワークエラーをインターセプトしてログします
- 再試行リンク -- バックオフを使用して失敗したネットワークリクエストを自動的に再試行します
HTTPリンク
import { createHttpLink } from '@apollo/client';import config from '../amplifyconfiguration.json';
const httpLink = createHttpLink({ uri: config.aws_appsync_graphqlEndpoint,});認証リンク
認証リンクはすべてのリクエストにCognito User Pools IDトークンを挿入します:
import { setContext } from '@apollo/client/link/context';import { fetchAuthSession } from 'aws-amplify/auth';
const authLink = setContext(async (_, { headers }) => { try { const session = await fetchAuthSession(); const token = session.tokens?.idToken?.toString(); return { headers: { ...headers, authorization: token || '', }, }; } catch (error) { console.error('Auth session error:', error); return { headers }; }});fetchAuthSession()はすべてのリクエストで呼び出されるため、トークンが常に新鮮であることが保証されます。Amplifyはリフレッシュトークンを使用して有効期限切れのアクセストークンを自動的にリフレッシュします。
エラーリンク
エラーリンクはすべてのGraphQLおよびネットワークエラーをグローバルにインターセプトします:
import { onError } from '@apollo/client/link/error';
const errorLink = onError(({ graphQLErrors, networkError }) => { if (graphQLErrors) { for (const { message, locations, path } of graphQLErrors) { console.error( `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}` );
if (message.includes('Unauthorized') || message.includes('401')) { // Token expired or invalid -- redirect to sign-in } } }
if (networkError) { console.error(`[Network error]: ${networkError}`); }});一般的なAppSyncエラー:
| エラーメッセージ | 原因 | アクション |
|---|---|---|
Unauthorizedまたは401 | 有効期限切れまたは欠落している認証トークン | サインインページにリダイレクト |
ConditionalCheckFailedException | ミューテーション入力に欠落または古い_version | 再クエリして最新の_versionを取得してから再試行 |
ConflictUnhandled | 競合解決がミューテーションを拒否した | 再クエリして新鮮なデータで再試行 |
Network error | 接続性の問題 | 再試行リンクが自動的に処理 |
再試行リンク
import { RetryLink } from '@apollo/client/link/retry';
const retryLink = new RetryLink({ delay: { initial: 300, max: 5000, jitter: true, }, attempts: { max: 3, retryIf: (error) => !!error, },});ネットワークエラーで最大3回、指数バックオフで再試行します。jitter: true設定は、thundering herd問題を防ぐためにランダム性を追加します。
すべてを組み合わせる
4つのリンクをすべて単一のApollo Clientインスタンスに組み合わせます:
import { ApolloClient, InMemoryCache, createHttpLink, from,} from '@apollo/client';
export const apolloClient = new ApolloClient({ link: from([retryLink, errorLink, authLink, httpLink]), cache: new InMemoryCache(),});リンクチェーン順序
from()関数は、送信リクエストで左から右に、受信応答で右から左にリンクを組み立てます:
Request --> RetryLink --> ErrorLink --> AuthLink --> HttpLink --> AppSyncResponse <-- RetryLink <-- ErrorLink <-- AuthLink <-- HttpLink <-- AppSync- RetryLinkが最初 -- チェーン全体をラップするため、ダウンストリームリンクまたはネットワークリクエストが失敗した場合、RetryLinkは完全なチェーン(認証トークンの再取得を含む)を再実行できます
- ErrorLinkが2番目 -- すべてのエラーを見ることができ、ログまたはリダイレクトできます
- AuthLinkが3番目 -- HTTPリクエストの直前にCognitoトークンを挿入します
- HttpLinkが最後 -- 実際のリクエストをAppSyncに送信します
Reactに接続
アプリケーションをApolloProviderでラップして、クライアントをすべてのコンポーネントで利用可能にします:
import { ApolloProvider } from '@apollo/client';import { apolloClient } from './apolloClient';
function App() { return ( <ApolloProvider client={apolloClient}> {/* Your app components can now use useQuery, useMutation, etc. */} </ApolloProvider> );}ApolloProvider内のコンポーネントは、Apolloの Reactフック(useQuery、useMutation)を使用してAppSync APIと対話できます。
サインアウトとキャッシュクリーンアップ
ユーザーがサインアウトするとき、Apollo Clientのメモリ内キャッシュをクリアして、次のユーザーが古いデータを見ないようにする必要があります:
import { signOut } from 'aws-amplify/auth';import { apolloClient } from './apolloClient';
async function handleSignOut() { // 1. Clear Apollo Client's in-memory cache await apolloClient.clearStore();
// 2. Sign out from Amplify (clears Cognito tokens) await signOut();}主な詳細:
clearStore()はメモリ内キャッシュをクリアし、アクティブなすべてのクエリをキャンセルします。キャッシュをクリアしてアクティブなすべてのクエリを再フェッチしたい場合は、代わりにresetStore()を使用してください。- 順序が重要: 最初にキャッシュをクリアしてから、サインアウトします。最初にサインアウトすると、認証トークンが既に無効化されているため、
clearStore()が再フェッチをトリガーする可能性があります。 - ローカルキャッシュを追加する場合(ローカルキャッシュの追加ページで説明)、サインアウト関数も永続キャッシュをパージする必要があります。
完全なapolloClie.tsセットアップファイル
上記のすべてを組み合わせた完全なsrc/apolloClient.tsファイルは以下のとおりです:
import { ApolloClient, InMemoryCache, createHttpLink, from,} from '@apollo/client';import { setContext } from '@apollo/client/link/context';import { onError } from '@apollo/client/link/error';import { RetryLink } from '@apollo/client/link/retry';import { fetchAuthSession } from 'aws-amplify/auth';import config from '../amplifyconfiguration.json';
// --- HTTP Link ---const httpLink = createHttpLink({ uri: config.aws_appsync_graphqlEndpoint,});
// --- Auth Link ---const authLink = setContext(async (_, { headers }) => { try { const session = await fetchAuthSession(); const token = session.tokens?.idToken?.toString(); return { headers: { ...headers, authorization: token || '', }, }; } catch (error) { console.error('Auth session error:', error); return { headers }; }});
// --- Error Link ---const errorLink = onError(({ graphQLErrors, networkError }) => { if (graphQLErrors) { for (const { message, locations, path } of graphQLErrors) { console.error( `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}` );
if (message.includes('Unauthorized') || message.includes('401')) { // Token expired or invalid -- redirect to sign-in } } }
if (networkError) { console.error(`[Network error]: ${networkError}`); }});
// --- Retry Link ---const retryLink = new RetryLink({ delay: { initial: 300, max: 5000, jitter: true }, attempts: { max: 3, retryIf: (error) => !!error },});
// --- Apollo Client ---// Link chain: RetryLink -> ErrorLink -> AuthLink -> HttpLink -> AppSyncexport const apolloClient = new ApolloClient({ link: from([retryLink, errorLink, authLink, httpLink]), cache: new InMemoryCache(),});リアルタイムサブスクリプションのセットアップ
サブスクリプションはAmplifyライブラリを使用します(Apolloではなく)。AppSyncは標準的なGraphQLサブスクリプションライブラリが処理できないカスタムWebSocketプロトコルを使用するためです。
Amplifyサブスクリプションクライアントを作成
Apollo Clientと一緒にAmplifyクライアントを作成します。アプリのスタートアップ時にAmplifyが既に構成されています:
import { generateClient } from 'aws-amplify/api';
const amplifyClient = generateClient();これで2つのクライアントが完成しました:
apolloClient-- クエリ、ミューテーション、キャッシング用amplifyClient-- サブスクリプションのみ用
サブスクリプションパターン:イベント時に再フェッチ(推奨)
最もシンプルで信頼性の高いアプローチ:サブスクリプションイベントが発火すると、リストクエリをサーバーから再フェッチします。
import { useQuery } from '@apollo/client';import { generateClient } from 'aws-amplify/api';import { useEffect } from 'react';import { LIST_POSTS } from './graphql/operations';
const amplifyClient = generateClient();
function PostList() { const { data, loading, error, refetch } = useQuery(LIST_POSTS);
useEffect(() => { const subscriptions = [ amplifyClient.graphql({ query: `subscription OnCreatePost { onCreatePost { id } }` }).subscribe({ next: () => refetch(), error: (err) => console.error('Create subscription error:', err), }), amplifyClient.graphql({ query: `subscription OnUpdatePost { onUpdatePost { id } }` }).subscribe({ next: () => refetch(), error: (err) => console.error('Update subscription error:', err), }), amplifyClient.graphql({ query: `subscription OnDeletePost { onDeletePost { id } }` }).subscribe({ next: () => refetch(), error: (err) => console.error('Delete subscription error:', err), }), ];
return () => subscriptions.forEach(sub => sub.unsubscribe()); }, [refetch]);
if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>;
const activePosts = data?.listPosts?.items?.filter( (post) => !post._deleted ) || [];
return ( <ul> {activePosts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> );}このパターンがうまく機能する理由:
- サブスクリプションペイロードはリスト全体を再フェッチするためだけに
idが必要なため、サブスクリプションは軽量のままです - キャッシュ操作ロジックはありません。再フェッチはサーバーとの一貫性を保証します
- イベントあたり1つの追加ネットワークラウンドトリップ。通常100ms未満であり、ほとんどのアプリケーションに対しては知覚できません
高度:直接キャッシュ更新パターン
サブスクリプションデータから再フェッチの代わりにApolloのキャッシュを直接更新する必要があるアプリケーションの場合、これにより追加のネットワークラウンドトリップが回避されますが、より多くのコードと慎重なキャッシュ管理が必要です。
import { useQuery } from '@apollo/client';import { generateClient } from 'aws-amplify/api';import { useEffect } from 'react';import { LIST_POSTS, POST_DETAILS_FRAGMENT } from './graphql/queries';import { apolloClient } from './apolloClient';
const amplifyClient = generateClient();
function PostListAdvanced() { const { data, loading, error } = useQuery(LIST_POSTS);
useEffect(() => { const sub = amplifyClient.graphql({ query: `subscription OnCreatePost { onCreatePost { id title content status rating _version _deleted _lastChangedAt createdAt updatedAt } }` }).subscribe({ next: ({ data }) => { const newPost = data.onCreatePost; apolloClient.cache.modify({ fields: { listPosts(existingData = { items: [] }) { const newRef = apolloClient.cache.writeFragment({ data: newPost, fragment: POST_DETAILS_FRAGMENT, }); return { ...existingData, items: [...existingData.items, newRef], }; }, }, }); }, error: (err) => console.error('Create subscription error:', err), });
return () => sub.unsubscribe(); }, []);
// ... render logic}推奨事項: 再フェッチパターンから開始してください。測定可能なパフォーマンス問題がある場合にのみ、直接キャッシュ更新に移動してください。
DataStoreの比較
| DataStore | Amplify + Apollo(ハイブリッド) |
|---|---|
DataStore.observe(Post).subscribe(...) | amplifyClient.graphql({ query: onCreatePost }).subscribe(...) |
DataStore.observeQuery(Post) | useQuery(LIST_POSTS) +サブスクリプション再フェッチ |
| モデルごとの自動サブスクリプション | イベントタイプごとの手動セットアップ(作成、更新、削除) |
| すべてのイベントタイプ用の単一の観察呼び出し | イベントタイプごとに別のサブスクリプション |
サブスクリプションのトラブルシューティング
一般的なサブスクリプションの問題
サブスクリプションは接続するが発火しない:
サブスクリプション名はスキーマと完全に一致する必要があります。AppSyncサブスクリプションはonCreateModelName、onUpdateModelName、onDeleteModelNameとして生成されます(キャメルケース)。AWSコンソールのAppSyncスキーマを確認してください。
サブスクリプションで認証エラー:
サブスクリプションクライアントを作成する前に、Amplifyを構成する必要があります。Amplify.configure(config)がアプリのスタートアップ時にgenerateClient()への呼び出しの前に実行されることを確認してください。
サブスクリプションは~5分の非アクティビティ後に切断:
これは正常な動作です。AmplifyのAWSAppSyncRealTimeProviderは自動再接続を処理します。アクション不要です。
開発では機能するがプロダクションでは機能しない:
amplifyconfiguration.json(またはaws-exports.js)設定がプロダクション環境で正しいこと、およびCORSがプロダクションドメインからのWebSocket接続を許可するようにAppSync APIに構成されていることを確認してください。
サブスクリプションは接続するがイベントを受け取らない(所有者ベースの認可):
モデルが所有者ベースの認可(@auth(rules: [{ allow: owner }]))を使用する場合、サブスクリプション内で$owner変数を渡す必要があります。それなしでは、サブスクリプションは正常に接続しますが、AppSyncはサイレントにすべてのイベントをフィルタリングアウトします。これは最も一般的な原因です「サブスクリプションは機能するが何も起こらない。」
現在の認証セッションから所有者値を取得し、変数として渡します:
import { fetchAuthSession } from 'aws-amplify/auth';
const session = await fetchAuthSession();const owner = session.tokens?.idToken?.payload?.sub as string;
(amplifyClient.graphql({ query: `subscription OnCreatePost($owner: String!) { onCreatePost(owner: $owner) { id } }`, variables: { owner },}) as any).subscribe({ next: () => refetch() });3つのサブスクリプションタイプ(onCreate、onUpdate、onDelete)すべてが、所有者ベースの認可モデルに$owner変数を必要とします。完全なReactコンポーネントパターンについては、高度なパターンページを参照してください。