git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/codex/graphql-client-patterns" ~/.claude/skills/intense-visions-harness-engineering-graphql-client-patterns-9dab15 && rm -rf "$T"
agents/skills/codex/graphql-client-patterns/SKILL.mdGraphQL Client Patterns
Structure GraphQL client code with fragments, cache normalization, and optimistic updates for responsive UIs
When to Use
- Building a React/Vue/Svelte app that consumes a GraphQL API
- Choosing between Apollo Client, urql, or lightweight clients
- Managing client-side cache for GraphQL data
- Implementing optimistic UI updates for mutations
- Organizing queries, mutations, and fragments across a large frontend
Instructions
- Co-locate queries with the components that use them. Define the query in the same file (or adjacent
file) as the component that renders the data. This makes data dependencies explicit..graphql
const GET_USER = gql` query GetUser($id: ID!) { user(id: $id) { id name ...UserAvatar } } ${USER_AVATAR_FRAGMENT} `; function UserProfile({ userId }: { userId: string }) { const { data, loading, error } = useQuery(GET_USER, { variables: { id: userId } }); if (loading) return <Skeleton />; if (error) return <ErrorBanner error={error} />; return <div>{data.user.name}</div>; }
- Use fragments to share field selections across queries. Fragments prevent field duplication and ensure components always receive the fields they need, regardless of which query fetches the data.
const USER_AVATAR_FRAGMENT = gql` fragment UserAvatar on User { id avatarUrl name } `; // Reuse in any query that needs avatar data
- Configure the normalized cache with
. Apollo Client'stypePolicies
normalizes byInMemoryCache
. Customize merge behavior, pagination, and key fields in__typename:id
.typePolicies
const cache = new InMemoryCache({ typePolicies: { User: { keyFields: ['id'], }, Query: { fields: { feed: offsetLimitPagination(), }, }, }, });
- Use optimistic responses for instant UI feedback. When the user performs a mutation, provide an
that mirrors the expected server response. The cache updates immediately; the real response replaces it when it arrives.optimisticResponse
const [toggleLike] = useMutation(TOGGLE_LIKE, { optimisticResponse: { toggleLike: { __typename: 'Post', id: postId, isLiked: !currentlyLiked, likeCount: currentlyLiked ? count - 1 : count + 1, }, }, });
- Choose the right cache update strategy after mutations:
- Automatic: If the mutation returns the modified object with its
andid
, the cache updates automatically.__typename
: Re-execute specific queries after the mutation. Simple but costs a network round-trip.refetchQueries
function: Manually read and write the cache for complex updates (e.g., adding an item to a list).update
- Automatic: If the mutation returns the modified object with its
const [addComment] = useMutation(ADD_COMMENT, { update(cache, { data: { addComment } }) { cache.modify({ id: cache.identify({ __typename: 'Post', id: postId }), fields: { comments(existing = []) { const newRef = cache.writeFragment({ data: addComment, fragment: COMMENT_FIELDS, }); return [...existing, newRef]; }, }, }); }, });
-
Handle loading and error states consistently. Create reusable patterns — a
wrapper component or custom hooks that standardize loading/error/empty states across the app.<QueryResult> -
Use
intentionally.fetchPolicy
(default): Read from cache, fetch only on miss. Best for stable data.cache-first
: Always fetch, update cache. Best for frequently changing data.network-only
: Return cached data immediately, then update from network. Best UX for lists.cache-and-network
: Skip the cache entirely. Use for one-off sensitive data.no-cache
-
Avoid over-fetching with field-level selections. Only request the fields the component needs. The normalized cache works best when queries request predictable field sets.
Details
Apollo Client vs. urql vs. lightweight clients: Apollo Client offers the richest cache and ecosystem but is the largest bundle (~40KB). urql is smaller (~15KB) with a plugin-based architecture (exchanges). For simple use cases,
graphql-request (~5KB) provides a fetch wrapper without caching.
Cache normalization explained: Apollo splits query results into individual objects keyed by
__typename:id. When a mutation returns an updated User, every query that references that user sees the update. This works only if every queried type has a stable id field.
Fragment colocation pattern (Relay-style): Each component declares a fragment for the data it needs. Parent components spread those fragments into their queries. This creates a clear contract: the component works if and only if its fragment is included.
Polling vs. subscriptions: Use
pollInterval for data that changes infrequently (dashboard stats). Use subscriptions for real-time data (chat messages, live scores). Polling is simpler to implement and does not require WebSocket infrastructure.
Common mistakes:
- Missing
in optimistic responses (cache cannot normalize without it)__typename - Using
for every mutation instead of leveraging automatic cache updatesrefetchQueries - Requesting entire objects when only a few fields are needed (over-fetching)
- Not handling the
state fromerror
(silent failures)useQuery
Source
https://www.apollographql.com/docs/react/
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.