GAP·MAP

GraphQL on the client

[ middle depth ]

How the client cache is keyed

Apollo, and urql with its Graphcache exchange, do not store the nested JSON a query returns. They flatten it into a normalized store: a flat table of objects that reference each other. Each object gets a cache ID from its __typename plus its id (or _id), so Todo:5 is stored once and every query that mentions that todo points at the same record with a __ref. Apollo adds __typename to every query for you so it can build those IDs.

The payoff is automatic consistency. A mutation whose response carries a todo's __typename and id merges into the matching record, and every active query reading it re-renders with no extra code. If an object comes back with no id, it is not stored on its own; it is embedded inside its parent and cannot be updated independently, which is a common source of stale views.

Why a new item doesn't appear after a create

Wrong "The add mutation returned the new todo with its id, so the list should show it." Auto-merge only reconciles the fields of an entity the cache already has. A brand-new Todo:6 is written to the store, but the cache cannot guess it belongs in the existing todos list, so that field is never touched.

Right you write it in. Add an update function to the mutation and append the new item to the list field with cache.modify (pushing the ref from cache.writeFragment) or writeQuery. The simpler option is refetchQueries: [GetTodos], correct but one network round-trip. If the same list is also shown filtered or sorted, each variant is a separate cache entry, so your write has to cover each one.

Fragments and colocation

A fragment is a named set of fields declared on a type (fragment NameParts on Person { firstName lastName }) and spread with ...NameParts. Colocation means each component declares the exact fields it needs in its own fragment, and parents compose their children's fragments. Components stay loosely coupled: a child can change its data needs without the parent rewriting one giant query. Apollo's useFragment reads a fragment's fields straight from the cache for one entity with no network request, re-rendering only when those fields change.

Optimistic UI, start to finish

Pass optimisticResponse and Apollo writes the predicted result into a separate optimistic layer immediately, so the UI updates before the server answers. The object must match the full response shape and include id and __typename; a newly created item needs a temporary id until the server assigns the real one. On the real response the layer is dropped and canonical data is written, and on a network or GraphQL error the layer rolls back to the prior state.

[ senior depth ]

Manual cache writes versus refetchQueries

Two ways to fix a list after a mutation, with a real tradeoff. cache.modify (or writeQuery) edits the store directly: instant, no network, but you own consistency. The cache keys a field by its arguments, so a filtered list, a sorted list, and each page of a paginated list are separate entries under their own keyArgs. Your write has to touch every variant that should include the new item, and a missed filtered view shows stale data. refetchQueries: [GetTodos] sidesteps that by reloading from the server: always correct, at the cost of a round-trip and server load. A common split is to let auto-merge cover edits to existing entities and reserve explicit writes or refetches for adds and removes. In Apollo Client 4 these cache APIs are unchanged; the React hooks now import from @apollo/client/react.

Normalized caching versus urql's document cache

Apollo's InMemoryCache and urql's @urql/exchange-graphcache normalize by identity, keeping every view of an entity in sync but requiring stable ids and field policies for lists and pagination. urql's default cacheExchange does document caching instead: it caches whole query results and invalidates any cached query that shares a __typename with a mutation result. That is zero-config but coarse. Its classic failure is the empty list: a query that returns [] has no __typename to key off, so a later insert never invalidates it. The fix is additionalTypenames: ['Todo'] in the operation context, telling urql which types the query depends on.

What persisted queries buy you

Wrong "Persisted queries are a security allowlist." Automatic Persisted Queries (APQ) send a SHA-256 hash the client computes instead of the full document. On a cache miss the server returns PersistedQueryNotFound, and the client retries with hash plus query, which the server then remembers. That cuts request bytes and, with useGETForHashedQueries, lets short hashed reads go out as GET so a CDN can cache them, since a normal GraphQL POST is not URL-addressable. Right APQ registers arbitrary operations at runtime, so it secures nothing. Safelisting needs a pre-registered persisted query list built at deploy time, against which the server rejects any operation not on the list. The hash saves bytes, not execution: the same operation still runs. The link is class-based in Apollo Client 4: new PersistedQueryLink({ sha256 }).

Pagination: keyArgs and the merge policy

Apollo stores a field's result under its arguments, so feed(offset:0) and feed(offset:20) are distinct entries. Without a merge policy the second fetchMore overwrites the first page, and Apollo warns about the missing merge function. A field policy fixes it: keyArgs names the arguments that create separate lists (keyArgs: false folds everything into one), and merge concatenates incoming pages. offsetLimitPagination() and relayStylePagination() from @apollo/client/utilities are the prebuilt versions. Offset pagination breaks on live lists: an insert or delete between page loads shifts every later offset, so rows get skipped or duplicated. Cursor pagination keys each page to a stable per-item cursor, so it survives inserts and deletes; the Relay connection shape carries edges { node cursor } and pageInfo { endCursor hasNextPage } to drive load-more.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

builds on

more Browser & Network

was this useful?