How fresh data reaches the cache, how each component shows a pending query, and where one component's choice reaches another
| 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 |
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.
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 |
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.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.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?).network-only fetch, and its result is written to the cache for every reader of that data.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.
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.
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.
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.
One component's display choice does not affect another component that reads the same data:
useQuery call has its own ObservableQuery. previousData, networkStatus, notifyOnNetworkStatusChange and fetchPolicy belong to that one call.refetch() affects only the caller. Others see the result of its cache write.networkStatus.| 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 |