src/skills/web-data-fetching-graphql-apollo/SKILL.md
Apollo Client GraphQL patterns - useQuery, useMutation, cache management, optimistic updates, subscriptions
npx skillsauth add agents-inc/skills web-data-fetching-graphql-apolloInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Use Apollo Client for GraphQL APIs. Provides automatic normalized caching, optimistic updates, and real-time subscriptions. Always use GraphQL Codegen for type safety. Configure
keyFieldson every entity type for proper cache normalization. UseerrorPolicy: "all"for graceful degradation. v3.9+ adds Suspense hooks; v4.0 moves React imports to@apollo/client/reactand addsdataStatefor type-safe query state.
<critical_requirements>
(You MUST use GraphQL Codegen for type generation - NEVER write manual TypeScript types for GraphQL)
(You MUST include __typename and id in all optimistic responses for cache normalization)
(You MUST configure type policies with appropriate keyFields for every entity type)
(You MUST use named constants for ALL timeout, retry, and polling values - NO magic numbers)
</critical_requirements>
Auto-detection: Apollo Client, useQuery, useMutation, useSubscription, useSuspenseQuery, useLoadableQuery, useBackgroundQuery, useFragment, ApolloClient, InMemoryCache, gql, GraphQL, optimistic updates, cache policies, createQueryPreloader
When to use:
When NOT to use:
Key patterns covered:
Detailed Resources:
Apollo Client is a comprehensive GraphQL client that provides intelligent normalized caching, reducing redundant network requests and keeping your UI consistent across components.
Core Principles:
Data Flow:
__typename + keyFields)Configure ApolloClient with InMemoryCache, type policies for cache normalization, and link chain for error handling and auth. Environment variables should use your framework's convention for the GraphQL endpoint.
const cache = new InMemoryCache({
typePolicies: {
User: { keyFields: ["id"] },
Product: { keyFields: ["sku"] }, // Non-default identifier
CartItem: { keyFields: false }, // Embed in parent, don't normalize
Query: {
fields: {
usersConnection: relayStylePagination(["filter"]),
},
},
},
});
Key decisions: keyFields determines how entities are identified in cache. Use ["id"] (default), custom field like ["sku"], composite ["authorId", "postId"], or false for embedded types.
See examples/core.md Pattern 1 for complete client setup with auth link, error link, and codegen configuration.
Declare data requirements with useQuery. Always handle loading, error, and empty states. Use cache-and-network for stale-while-revalidate behavior.
const { data, loading, error, refetch } = useQuery<GetUsersQuery, GetUsersQueryVariables>(
GET_USERS,
{
variables: { limit: DEFAULT_PAGE_SIZE },
fetchPolicy: "cache-and-network",
skip: !shouldFetch,
}
);
if (loading && !data) return <Skeleton />;
if (error) return <Error message={error.message} onRetry={() => refetch()} />;
if (!data?.users?.length) return <EmptyState />;
Why this pattern: loading && !data shows skeleton only on initial load (not background refetch). cache-and-network shows cached data immediately while refreshing from network.
See examples/core.md Pattern 2 for complete useQuery and useLazyQuery examples.
For mutations, decide between three cache update strategies: optimistic response (instant UI), update callback with cache.modify (manual cache update), or refetchQueries (simple but costs a network request).
const [createPost] = useMutation(CREATE_POST, {
optimisticResponse: {
createPost: {
__typename: "Post", // REQUIRED for normalization
id: `temp-${Date.now()}`, // Temporary ID, replaced by server response
title,
content,
},
},
update(cache, { data }) {
cache.modify({
fields: {
posts(existing = [], { toReference }) {
return [toReference(data.createPost), ...existing];
},
},
});
},
});
Critical: Always include __typename and id in optimistic responses. For deletes, use cache.evict() + cache.gc(). For simple cases, refetchQueries is fine.
See examples/core.md Pattern 3 for create, update, and delete mutation examples.
Type policies control how Apollo normalizes and retrieves cached data. This is where you configure cache identifiers, computed fields, pagination merging, and local state.
typePolicies: {
User: {
keyFields: ["id"],
fields: {
fullName: {
read(_, { readField }) {
return `${readField("firstName")} ${readField("lastName")}`;
},
},
},
},
Query: {
fields: {
isLoggedIn: { read() { return isLoggedInVar(); } },
},
},
}
Key patterns: keyFields for identification, merge for pagination, read for computed/local fields, keyArgs for separating cache entries per filter.
See examples/core.md Pattern 1 and examples/pagination.md for type policy examples.
Two approaches: Relay-style (cursor-based, use relayStylePagination) and offset-based (custom merge/read functions). Both require type policies for merging.
const { data, fetchMore } = useQuery(GET_USERS_CONNECTION, {
variables: { first: PAGE_SIZE },
});
const loadMore = () =>
fetchMore({
variables: { after: data.usersConnection.pageInfo.endCursor },
});
Key requirement: keyArgs must be set to separate cache entries per filter. Without it, different filtered queries overwrite each other.
See examples/pagination.md for infinite scroll with IntersectionObserver and custom offset pagination type policies.
Colocate data requirements with components using fragments. Parent queries include child fragments, so component changes don't require updating parent queries.
const USER_CARD_FRAGMENT = gql`
fragment UserCard on User {
id
name
email
avatar
}
`;
// Parent query includes child fragment
const GET_USERS = gql`
query GetUsers {
users {
...UserCard
}
}
${UserCard.fragments.user}
`;
See examples/fragments.md for fragment composition and examples/core.md Pattern 2 for fragments in queries.
Requires split link configuration: WebSocket for subscriptions, HTTP for queries/mutations. Use graphql-ws (not the deprecated subscriptions-transport-ws).
const splitLink = split(
({ query }) => {
const def = getMainDefinition(query);
return (
def.kind === "OperationDefinition" && def.operation === "subscription"
);
},
wsLink,
httpLink,
);
Important: Only create wsLink on the client side (typeof window !== "undefined"). Update cache in onData callback.
See examples/subscriptions.md for complete WebSocket setup and useSubscription with cache updates.
Use makeVar for simple client-side state that integrates with Apollo's reactivity system. Suitable for theme, auth status, cart items - not complex state.
const cartItemsVar = makeVar<string[]>([]);
const addToCart = (id: string) => cartItemsVar([...cartItemsVar(), id]);
// Component reacts automatically
const cartItems = useReactiveVar(cartItemsVar);
When to use reactive vars vs external state management: Reactive vars for simple Apollo-integrated state. For complex non-GraphQL state, use your client state management solution.
Four Suspense-enabled hooks for different loading patterns:
| Hook | Trigger | Use Case |
| ---------------------- | ---------------- | ---------------------------- |
| useSuspenseQuery | Component mount | Standard data loading |
| useLoadableQuery | User interaction | Hover/click prefetch |
| useBackgroundQuery | Parent mount | Parent triggers, child reads |
| createQueryPreloader | Route transition | Router loader integration |
Key difference from useQuery: No loading state - component suspends instead. Errors throw to Error Boundary.
See examples/suspense.md for complete examples of all four patterns.
Read fragment data directly from cache with automatic updates. Useful for components that only need a subset of cached entity data.
const { data: user, complete } = useFragment({
fragment: USER_CARD_FRAGMENT,
from: userRef,
});
if (!complete) return <Skeleton />;
Why useful: Reads directly from cache without additional queries, complete flag indicates if all fragment fields are available.
<version_migration>
Apollo Client v4 (released September 2025, latest v4.1.6) introduces significant breaking changes. A codemod handles most mechanical changes: npx @apollo/client-codemod-migrate-3-to-4
| Change | v3 | v4 |
| ----------------------------- | --------------------- | ---------------------------------------------------------- |
| React hook imports | @apollo/client | @apollo/client/react |
| Client uri option | Allowed directly | Must use explicit HttpLink |
| name/version | Top-level on client | clientAwareness: { name, version } |
| notifyOnNetworkStatusChange | Default false | Default true |
| Error classes | ApolloError | CombinedGraphQLErrors, ServerError, ServerParseError |
| Observable library | zen-observable | rxjs (peer dependency) |
| Link creation | createHttpLink() | new HttpLink() (class-based) |
| from()/concat()/split() | Standalone functions | ApolloLink.from() static methods |
| connectToDevTools | Client option | Replaced by devtools: { enabled: true } |
| Local resolvers | resolvers on client | Explicit LocalState class |
dataState Property (v4)const { data, dataState } = useQuery(GET_USER);
// dataState: "empty" | "partial" | "streaming" | "complete"
if (dataState === "complete") {
// TypeScript knows data is fully populated
}
import { CombinedGraphQLErrors, ServerError } from "@apollo/client";
if (CombinedGraphQLErrors.is(error)) {
error.errors.forEach(({ message }) => console.error(message));
}
if (ServerError.is(error)) {
console.error(`Server responded with ${error.statusCode}`);
}
See Apollo Client 4 Migration Guide for complete details.
</version_migration>
<red_flags>
High Priority Issues:
__typename in optimistic responses - Cache normalization fails silentlyid in query responses - Apollo cannot normalize data without identifierskeyArgs in paginated type policies - Different filters overwrite each other in cache@apollo/client - Must use @apollo/client/react in v4uri option directly on ApolloClient - Must use explicit HttpLink in v4Medium Priority Issues:
errorPolicy: "all" - Partial data is often better UX than complete failurerefetchQueries for simple updates - Direct cache updates with cache.modify are more efficientnetwork-only for all queries - cache-and-network provides better UX (stale-while-revalidate)useQuery/useMutation generics - Loses type safety benefitsCommon Mistakes:
graphql-codegen after schema changescache.writeQuery when cache.modify is more appropriate (writeQuery replaces entire query result)update callback (for cache updates) with onCompleted callback (for side effects like navigation)notifyOnNetworkStatusChange when showing refetch/fetchMore loading statesGotchas & Edge Cases:
fetchMore pagination requires type policy merge functions - without them, new data replaces oldcache.evict must be followed by cache.gc() to clean up orphaned referencesreadField in type policies is safer than direct property access (handles References)refetchQueries runs after update callback, not beforepollInterval: 0 disables polling; omit the option entirely for no pollingkeyFields: false embed objects in parent (no separate cache entry)split - queries/mutations stay on HTTPuseSuspenseQuery has no loading state - it suspends; errors throw to Error BoundaryqueryRef from useLoadableQuery must be passed to useReadQuery inside a Suspense boundarycreateQueryPreloader must be called outside the React tree (e.g., router loaders)notifyOnNetworkStatusChange defaults to true - may cause unexpected re-rendersrxjs is a required peer dependency - must install explicitlyApolloError class removed - use CombinedGraphQLErrors.is() and ServerError.is() for type-checkingfrom(), concat(), split() are static methods on ApolloLink, not standalone functionscreateHttpLink() removed - use new HttpLink() constructor insteaduseMutation types now enforce required variables at the call site</red_flags>
<critical_reminders>
(You MUST use GraphQL Codegen for type generation - NEVER write manual TypeScript types for GraphQL)
(You MUST include __typename and id in all optimistic responses for cache normalization)
(You MUST configure type policies with appropriate keyFields for every entity type)
(You MUST use named constants for ALL timeout, retry, and polling values - NO magic numbers)
(For v4: You MUST import React hooks from @apollo/client/react - NOT from @apollo/client)
Failure to follow these rules will cause cache inconsistencies, type drift, and production bugs.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety