How fresh data reaches the cache, how each component shows a pending query, and where one component's choice reaches another
Several components often read the same data through the one Apollo cache. Two questions come up for each query, and they have different owners:
Question
Owner
Rule
How does fresh data get into the cache? (fetching and freshness)
Shared. Every component that reads the data benefits
One recipe per kind of data. Deviate only for a data-freshness need, never for a display need
What does this component show while a request is pending?
The component
Free to differ from its neighbours, as long as it never blanks data it already has
The first is about the cache, so one component's choice reaches all the others. The second is about one component's own render, so it reaches nobody. Keep them apart: a fetchPolicy picked to make a spinner behave is a freshness decision made for a display reason, and it changes what every other reader of that data gets.
This page describes the client as it is configured today. createApolloClient sets no defaultOptions, so every option below is Apollo Client 4's own default unless a call passes one. If a shell later pins defaults, these rules still hold; only the per-call options you need to write change.
Fetching and freshness
Apollo 4's fetchPolicy defaults to cache-first: a query that finds all its data in the cache makes no request. That is right for most data. A live screen also needs a way to catch up, and there are two, which work together:
Recipe
What it does
Use it when
A subscription
Writes every change to the cache while the component is mounted
The data changes while the user watches. See Pub/Sub
cache-and-network with nextFetchPolicy: "cache-first"
Shows what the cache has, fetches once on mount to catch up on anything missed, then goes back to cache-first so later cache writes do not trigger more requests
The user can arrive after the data changed, and a subscription alone would not have told them
Neither (plain cache-first)
Reads the cache
The data is client-only, or another mounted component already subscribes to it
A live screen, such as a chatroom list, uses both: the query catches up on mount and the subscription keeps it current.
nextFetchPolicy applies after the first request, and Apollo resets it to the original policy when the variables change, so a new filter catches up once too.
Set fetchPolicy on the query that needs the catch-up, not shell-wide. A global cache-and-network makes every query in every remote refetch on mount.
Avoid network-only. It fetches on every mount and gives the same result as cache-and-network with a worse loading state. Use cache-and-network with nextFetchPolicy.
no-cache only for a root whose data no fragment reads. It writes nothing, so a child's useFragment finds nothing (see Why is useFragment incomplete?).
Do not poll to stay fresh when a subscription exists. A poll is a network-only fetch, and its result is written to the cache for every reader of that data.
Showing a pending query
useQuery returns data, previousData, dataState, networkStatus and loading. Apollo 4 sets notifyOnNetworkStatusChange to true unless a call says otherwise, so loading is true for every request in flight: the first load, a refetch(), a poll, fetchMore, and a change of variables. data stays put on a refetch or poll, and on a variable change it becomes the new variables' cached result, or undefined while there is none, with the old result in previousData.
The rule: a skeleton only when there is no data
Never write if (loading) return <Skeleton />. It blanks a screen that is only refreshing, and it did exactly that to the Dispatch chatroom list in March 2026. Show a skeleton when the component has nothing to show. Pick one of three recipes for what happens the rest of the time.
A. Keep showing the old data. The default for lists. Rows stay put and new data replaces them when it arrives.
B. Keep the old data and mark it stale. Show the rows, dimmed or with a spinner, while a refresh is in flight.
notifyOnNetworkStatusChange: true is already Apollo 4's default. Keep it in the call anyway, so this component does not break if a shell pins the default to false.
C. Skeleton when the old data is the wrong data. After a filter or tab change, the previous rows answer a different question, so do not show previousData. Show a skeleton until the new variables have data, and keep the data through a refetch or poll.
To keep loading itself false during a refetch or poll in one component, pass notifyOnNetworkStatusChange: false on that call. It changes what that component re-renders for, and nothing else.
A hook that wraps a query and adds mapping or derived state should return flags with their own names (isFirstLoad, isRefreshing), never a bare loading that means something different from Apollo's.
What does not reach other components
One component's display choice does not affect another component that reads the same data:
Each useQuery call has its own ObservableQuery.previousData, networkStatus, notifyOnNetworkStatusChange and fetchPolicy belong to that one call.
A cache write updates other readers' data, not their pending state. They re-read the cache and re-render with the new data, with no request.
refetch() affects only the caller. Others see the result of its cache write.
Request deduplication shares the network request, not the state. If two components ask for the same query and variables while one request is in flight, Apollo sends one request. Each keeps its own networkStatus.
Where one component's choice does reach another
These do reach other components, so treat them as shared decisions:
Action in component A
Effect on component B that reads the same data
What to do
refetch(), a cache-and-network fetch or a poll
B's data updates in place; its pending state does not change
Expected. Freshness is shared
A mutation with refetchQueries: [Doc], or client.refetchQueries({ include: [Doc] })
Every active query on Doc, for all variables, refetches and goes into its refetch state. A no-cache query refetches too
Return the changed entities and update the cache first. If a refetch is still needed, narrow it (see Mutation order)
client.resetStore()
Every query loses its data and refetches
Only when the user's identity changes
A writes a smaller selection of a list, or of an object with no identity
Lists are replaced on write, because no merge policy exists. If B reads more fields than A wrote, B goes to the network on its own
Give both roots the same arguments and identity. A list shared between roots needs keyArgs and merge in createPreparedInMemoryCache, with a test
fetchMore with a merge policy
The merged list is written to the shared field, so B sees the extra pages
Page a list from one root
no-cache
Nothing is written, so B is not affected, but A's own fragments read nothing
Only for data no fragment reads
Related documentation
GraphQL Cache - Identity, updating the cache after mutations and subscriptions, and the order to reach for
Backend Responses - What a mutation or subscription should return so the frontend needs none of the workarounds above