Name:
interface
Value:
Amplify has re-imagined the way frontend developers build fullstack applications. Develop and deploy without the hassle.

Page updated May 1, 2026

LegacyYou are viewing Gen 1 documentation. Switch to the latest Gen 2 docs →

Advanced patterns

This page covers four advanced topics: migrating React components from imperative DataStore calls to declarative Apollo hooks, composite and custom primary keys, GraphQL codegen for type-safe operations, and an honest accounting of DataStore features that have no direct Apollo Client equivalent.

The React-specific hooks (useQuery, useMutation) shown in other sections of this guide are not available in Angular, vanilla JavaScript, or Vue. Use the imperative Apollo Client APIs (apolloClient.query(), apolloClient.mutate()) instead. These are the same patterns shown in the "imperative" examples on the Migrate CRUD operations page.

Composite and custom primary keys

Amplify supports three identifier modes for models. Each mode changes how you query, update, and delete records -- and each requires different Apollo Client configuration.

The three identifier modes

Identifier ModeGen 1 SchemaGraphQL Get InputCreate Input
Default auto-generated IDNo @primaryKey directivegetModel(id: ID!)id auto-generated by AppSync
Custom single-field PK@primaryKey(sortKeyFields: []) on a custom fieldgetModel(id: ID!)id required in create input
Composite PK@primaryKey(sortKeyFields: ["field2"])getModel(field1: ..., field2: ...)All PK fields required

Default (auto ID)

This is the default mode when you do not use @primaryKey on your model. AppSync auto-generates a UUID id field. No special migration is needed -- the standard CRUD patterns from the Migrate CRUD operations page apply directly.

Gen 1 schema:

# amplify/backend/api/<your-api>/schema.graphql
type Post @model @auth(rules: [{ allow: owner }]) {
id: ID!
title: String!
content: String
status: String
}

Custom single-field PK

When your model defines a custom primary key field, the id is no longer auto-generated. You must provide it explicitly in create mutations.

Gen 1 schema:

# amplify/backend/api/<your-api>/schema.graphql
type Product @model @auth(rules: [{ allow: owner }]) {
id: ID! @primaryKey
sku: String!
name: String!
price: Float
}

Apollo Client:

const { data } = await apolloClient.mutate({
mutation: CREATE_PRODUCT,
variables: {
input: {
id: 'PROD-001', // REQUIRED -- you must provide this
sku: 'SKU-12345',
name: 'Widget',
price: 29.99,
},
},
});

Composite PK

This mode requires the most migration work. When a model uses @primaryKey with sortKeyFields, ALL primary key fields become required arguments.

Gen 1 schema:

amplify/backend/api/<your-api>/schema.graphql
type StoreBranch @model @auth(rules: [{ allow: owner }]) {
tenantId: ID! @primaryKey(sortKeyFields: ["branchName"])
branchName: String!
address: String
phone: String
}

Apollo Client queries and mutations:

// Query by composite key -- both fields as separate variables
const { data } = await apolloClient.query({
query: GET_STORE_BRANCH,
variables: { tenantId: 'tenant-123', branchName: 'Downtown' },
});
// Update -- ALL PK fields + _version required in input
await apolloClient.mutate({
mutation: UPDATE_STORE_BRANCH,
variables: {
input: {
tenantId: 'tenant-123',
branchName: 'Downtown',
address: '456 New St',
_version: data.getStoreBranch._version,
},
},
});

Cache configuration for composite keys (typePolicies)

This is the critical configuration step that is easy to miss. Apollo's InMemoryCache uses __typename:id as the default cache key. Models with composite keys will NOT cache or normalize correctly without explicit keyFields configuration.

import { InMemoryCache } from '@apollo/client';
const cache = new InMemoryCache({
typePolicies: {
// Default models work automatically
Post: { keyFields: ['id'] },
// Composite key models NEED explicit keyFields
StoreBranch: { keyFields: ['tenantId', 'branchName'] },
// Custom single-field PK
Product: { keyFields: ['sku'] },
},
});

Warning signs that keyFields is missing: queries return stale data after mutations, Apollo DevTools shows duplicate entries, cache.readQuery returns null for records you know exist.

GraphQL codegen for type-safe operations

The CRUD examples in earlier pages use (post: any) casts. This section shows how to eliminate those.

Step 1: Generate GraphQL operations

amplify codegen

This generates TypeScript files in src/graphql/ containing your operations as string constants.

Step 2: Wrap with gql() and TypeScript types

Create a typed operations file that wraps the generated strings:

Complete typed-operations.ts example
src/graphql/typed-operations.ts
import { gql, TypedDocumentNode } from '@apollo/client';
import { getPost as getPostString, listPosts as listPostsString } from './queries';
import { createPost as createPostString, updatePost as updatePostString, deletePost as deletePostString } from './mutations';
export interface Post {
id: string;
title: string;
content: string;
status: string;
rating: number;
createdAt: string;
updatedAt: string;
_version: number;
_deleted: boolean | null;
_lastChangedAt: number;
}
export interface GetPostData { getPost: Post | null; }
export interface GetPostVars { id: string; }
export interface ListPostsData {
listPosts: { items: Post[]; nextToken: string | null; };
}
export interface ListPostsVars {
filter?: Record<string, unknown>;
limit?: number;
nextToken?: string;
}
export interface CreatePostData { createPost: Post; }
export interface CreatePostVars {
input: { title: string; content: string; status?: string; rating?: number; };
}
export interface UpdatePostData { updatePost: Post; }
export interface UpdatePostVars {
input: { id: string; _version: number; title?: string; content?: string; };
}
export interface DeletePostData { deletePost: Post; }
export interface DeletePostVars {
input: { id: string; _version: number; };
}
export const GET_POST: TypedDocumentNode<GetPostData, GetPostVars> = gql(getPostString);
export const LIST_POSTS: TypedDocumentNode<ListPostsData, ListPostsVars> = gql(listPostsString);
export const CREATE_POST: TypedDocumentNode<CreatePostData, CreatePostVars> = gql(createPostString);
export const UPDATE_POST: TypedDocumentNode<UpdatePostData, UpdatePostVars> = gql(updatePostString);
export const DELETE_POST: TypedDocumentNode<DeletePostData, DeletePostVars> = gql(deletePostString);

Step 3: Use type-safe hooks

With TypedDocumentNode, Apollo hooks automatically infer data and variable types:

What is lost -- features with no direct equivalent

DataStore provided a managed sync lifecycle with rich event hooks. Apollo Client is a query/cache layer, not a sync engine. This section documents every DataStore feature that has no direct Apollo equivalent, with honest workaround ratings.

Hub events

DataStore dispatched 9 distinct events via Hub. Of the 9:

CategoryCountDetails
Fully replaced0None have a direct Apollo equivalent
Partially replaced2networkStatus (use browser APIs), subscriptionsEstablished (monitor subscription callbacks)
No equivalent7syncQueriesStarted, syncQueriesReady, modelSynced, outboxMutationEnqueued, outboxMutationProcessed, outboxStatus, storageSubscribed

The 7 with no equivalent describe sync engine behavior, and Apollo Client does not have a sync engine.

Selective sync (syncExpressions)

DataStore's syncExpressions let you filter which records synced from server to local store. Apollo Client has no equivalent.

Lifecycle methods

MethodApollo EquivalentRating
DataStore.start()None (Apollo queries on demand)None
DataStore.stop()Unsubscribe manually; apolloClient.stop() cancels in-flightNone
DataStore.clear()apolloClient.clearStore() + persistor.purge()Partial

Conflict handler configuration

This IS covered in the migration guide. Conflicts are handled server-side. Rating: Full (different location, same capability).

Summary

CategoryFully ReplacedPartially ReplacedNo Equivalent
Hub lifecycle events (9 total)027
Selective sync010
Lifecycle methods (3 total)012
Conflict handlers100
Totals149

Practical guidance

If your app depends heavily on Hub events for UI state (showing sync progress indicators, outbox status badges), plan additional custom implementation work. For most apps migrating to Apollo Client, these features are not needed because there is no local sync to monitor. The loss is real but the impact is low.