Client-Side: Queries, Mutations & the Normalized Cache
useQuery & useMutation
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) { id name email }
}
`;
function Profile({ id }) {
const { loading, error, data } = useQuery(GET_USER, { variables: { id } });
if (loading) return <Spinner />;
if (error) return <Error />;
return <div>{data.user.name}</div>;
}
const [updateUser] = useMutation(UPDATE_USER);
// called explicitly, e.g. onSubmit={() => updateUser({ variables: { id, name } })}useQuery runs automatically on render (loading/error/data states drive re-renders); useMutation returns a function you call explicitly, usually from a user action. Variables ($id) parameterize a query without string-interpolating values into it — safer and enables correct cache keying.
Normalized Cache
Data is stored flat, keyed by type + ID — not as duplicated nested blobs per query. Fetch the same User via two different queries and it's stored once; updating it (via a mutation with a matching ID in the response) automatically updates every part of the UI displaying that user, no manual refetch needed. Non-standard ID fields need a keyFields config or normalization silently breaks.
Cache Reconciliation Fallbacks
When a mutation's effect can't cleanly reconcile automatically, an explicit update function patches the cache manually; refetchQueries is the simpler, blunter fallback — just re-run the affected query from scratch. optimisticResponse updates the UI immediately assuming success, rolling back only if the mutation actually fails — for snappy perceived responsiveness.
Fetch Policies & Fragments
cache-first prefers cached data (network only on a miss); network-only always refetches; cache-and-network shows cached data immediately while refreshing in the background. Fragments define a reusable field subset per component, composed into a page-level query — co-locating data requirements with the component that needs them.
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free