skip to content

Queries and Mutations

useQuery and useMutation with their options — variables, fetchPolicy, skip — and the loading, error, and data states they expose. Interviewers ask how you refresh the UI after a mutation: refetchQueries, optimisticResponse, or a manual cache update.

on this pageshow

explore

questions

6

In Apollo Client 4, what does useQuery return for an issue list while loading, on error and on success, and how do you render each?

level: juniorimportance: must knowfreq 62%

answer

  1. four fields, not three
  2. the states can overlap
  3. loading can sit beside cached rows
  4. a failed refetch keeps the old rows
  5. dataState: empty, partial, streaming, complete

basics

~20 s

Apollo Client 4's useQuery returns loading, error, data and dataState, which can overlap: cached rows arrive with loading true, and a failed refetch keeps rows beside error. Render data when present, else the error or a spinner.

solid answer

~40 s

`useQuery` from `@apollo/client/react` returns `data`, `loading`, `error`, `networkStatus` and, new in 4.x, `dataState` (`empty`, `partial`, `streaming` or `complete`). On a first fetch with nothing cached you get `loading: true` and `data: undefined`. On success `loading` is false and `dataState` is `complete`. The states overlap, though. With `cache-and-network`, cached rows arrive while `loading` is still true. Because `notifyOnNetworkStatusChange` now defaults to `true`, a `refetch` or a poll flips `loading` back to true while `data` still holds the old rows. A failed refetch for the same variables keeps those rows and sets `error` beside them. So render rows whenever `data` exists (`dataState === "complete"` also narrows the TypeScript type). Show a full error only when there is no data, and a skeleton only when there is neither.

code

tsx · 29 lines
tsx
import { gql, type TypedDocumentNode } from "@apollo/client";
import { useQuery } from "@apollo/client/react";

type Issue = { __typename: "Issue"; id: string; title: string; status: "OPEN" | "CLOSED" };

const ISSUES: TypedDocumentNode<{ issues: Issue[] }> = gql`
  query Issues {
    issues { id title status }
  }
`;

export function IssueList() {
  const { data, dataState, loading, error } = useQuery(ISSUES);

  if (dataState === "complete") {
    return (
      <section aria-busy={loading}>
        {error && <p role="alert">Refresh failed: {error.message}</p>}
        <ul>
          {data.issues.map((issue) => (
            <li key={issue.id}>{issue.title} ({issue.status})</li>
          ))}
        </ul>
      </section>
    );
  }
  if (error) return <p role="alert">Could not load issues: {error.message}</p>;
  return <p>Loading issues...</p>;
}

go deeper

for a junior

Recall the fields useQuery returns and that Apollo Client 4 hooks come from @apollo/client/react. Show you render rows from data rather than assuming loading and data never coexist.

for a middle

Explain why loading can be true beside cached data: cache-and-network, plus refetch and polling now that notifyOnNetworkStatusChange defaults to true. Use dataState and networkStatus to tell those cases apart.

for a senior

Show a render order that never blanks a list the user was reading: stale rows with an inline banner on a failed refetch, and a full error state only without data. Treat reverting notifyOnNetworkStatusChange as a migration lever, not a fix.

for a principal

Frame loading and error states as a product rule: which screens may show stale rows during a refresh, and whether a shared list component should own that rule instead of every team re-deciding it.

## What useQuery hands back In Apollo Client 4 the React hooks live in the **`@apollo/client/react`** entry point; the core `@apollo/client` package no longer exports React APIs. You call `useQuery(ISSUES)` with a query document, and the hook returns one result object that is re-created whenever the query's state changes. The fields you render from are: | Field | What it tells you | Typical values | |---|---|---| | `data` | The query result, if Apollo has one | `undefined` until something arrives | | `dataState` | How complete `data` is | `empty`, `partial`, `streaming`, `complete` | | `loading` | Whether a request for this query is in flight | `true` / `false` | | `error` | One error object for the latest execution | `undefined` when it succeeded | | `networkStatus` | Why the query is busy | a `NetworkStatus` number | `dataState` is new in 4.x. `partial` only appears when you opt into `returnPartialData`, and `streaming` only with `@defer`, so for an ordinary issue list you will mostly see `empty` and `complete`. ## The plain lifecycle For an issue list mounted with the default `cache-first` policy and an empty cache, the hook moves through three states: 1. **First render:** `loading: true`, `data: undefined`, `dataState: "empty"`, `networkStatus` is `NetworkStatus.loading`. 2. **Success:** `loading: false`, `data` holds `{ issues: [...] }`, `dataState: "complete"`, `networkStatus` is `ready`. 3. **Failure on that first request:** `loading: false`, `error` is set, `data` stays `undefined`, `networkStatus` is `error`. If the whole list is already cached, `cache-first` skips step 1 and the first render is already complete. ## Why the states are not mutually exclusive The classic tutorial shape, `if (loading) return <Spinner />; if (error) return <Error />; return <List />`, assumes that only one flag is ever true. In Apollo Client 4 that assumption breaks in several everyday cases: - **`cache-and-network`** renders the cached rows immediately and still sends a request, so `data` is complete while `loading` is `true`. - **Refetching and polling** re-render with `loading: true` because **`notifyOnNetworkStatusChange` now defaults to `true`**. The rows the user was reading are still in `data`, and `networkStatus` says `refetch` or `poll`. - **A failed refetch** for the same query and variables keeps the previous result and adds `error` next to it, so `data` and `error` are both set. - **A skipped query** (passing `skipToken`) returns `loading: false` with `data: undefined` and `dataState: "empty"`, so "not loading" does not mean "has data". - **The default `errorPolicy` of `none`** discards partial data from a response that carries GraphQL errors, so a first load that fails has no rows at all. A component that returns a spinner whenever `loading` is true therefore blanks the list on every refresh. One that returns an error screen whenever `error` is set throws away rows that were still perfectly readable. ## A render order that survives all of them Check for data first, then decide what to overlay: 1. If `dataState === "complete"`, render the rows. Add a small busy indicator when `loading` is true and an inline banner when `error` is set. 2. Otherwise, if `error` is set, render a full error state, because there is nothing else to show. 3. Otherwise render the skeleton or spinner. The `dataState` check doubles as TypeScript narrowing: inside that branch `data` is the complete result type, not `undefined`. ## networkStatus when you need more detail `networkStatus` is a `NetworkStatus` enum imported from `@apollo/client`. Its values separate situations that `loading` lumps together: - `loading` (1) for a first execution and `setVariables` (2) for a change of variables. - `fetchMore` (3), `refetch` (4) and `poll` (6) for the different kinds of refresh. - `ready` (7) when idle, `error` (8) after a failure, and `streaming` (9) while a deferred response is still arriving. A common use is showing a thin progress bar only for `NetworkStatus.refetch` while keeping the list interactive. ## Mistakes interviewers listen for - Treating `loading === false` as a promise that `data` is defined, and crashing on `data.issues` when the query was skipped or failed. - Importing `useQuery` from `@apollo/client`, which is the Apollo Client 3 path. - Reaching for an `onCompleted` callback on `useQuery` to react to new data: Apollo Client 4 removed `onCompleted` and `onError` from `useQuery` and `useLazyQuery`, so you derive UI from `data` and `error` during render. - Turning `notifyOnNetworkStatusChange` off globally just to hide refresh spinners. It works, since it restores the 3.x behaviour, but it also hides refresh progress that the render order above can show properly.

  • What does networkStatus tell you that loading does not?
    `loading` is a single boolean, while `networkStatus` says why the query is busy: `loading` for a first run, `setVariables` after new variables, `fetchMore`, `refetch`, `poll`, then `ready`, `error` or `streaming`. Comparing it with `NetworkStatus.refetch`, imported from `@apollo/client`, lets you show a small refresh indicator while keeping the issue list on screen.
  • How does the result of useMutation differ from useQuery's?
    `useMutation` returns a tuple: a `mutate` function and a result with `data`, `loading`, `error`, `called`, `client` and `reset`. Nothing runs until you call `mutate`, so `called` and `loading` start false. `mutate` returns a promise that rejects on failure under the default `errorPolicy`, and `reset` clears the hook's state without touching the cache.
  • What does useQuery return when the query is skipped with skipToken?
    It returns `loading: false`, `data: undefined`, `dataState: "empty"` and `networkStatus` `ready`, because the query is parked in `standby` and never sent. A component that only checks `loading` before reading `data.issue` crashes in that state, so check `data` or `dataState` instead.

saying these in an interview costs you the question

  • When loading is false, data is always defined.
  • loading, error and data are mutually exclusive, so check loading first and return a spinner.
  • Whenever error is set, data is undefined and the list must be replaced by an error screen.
  • In Apollo Client 4 useQuery is still imported from @apollo/client.
  • useQuery's onCompleted callback is the place to react to fresh data in Apollo Client 4.
open as a page

In Apollo Client 4, how do you choose between refetchQueries, an update function and the mutation payload to show a newly created issue?

level: middleimportance: must knowfreq 58%

basics

~20 s

In Apollo Client 4 a mutation's normalized payload updates issues already cached but never adds one to a list. Use an update function to insert the new issue without a request, or refetchQueries when the server must decide membership or order.

open as a page

In Apollo Client 4, how do the cache-first, cache-and-network, network-only and no-cache fetch policies differ for an issue list, and when do you pick each?

level: middleimportance: should knowfreq 52%

basics

~20 s

Apollo Client 4 defaults to cache-first: serve a complete cached result, fetch only on a miss. cache-and-network shows cached rows and always fetches; network-only always fetches but still writes and watches the cache; no-cache bypasses the cache entirely.

open as a page

In Apollo Client 4, how does useMutation's optimisticResponse make a closeIssue click show instantly, and what happens to the cache if the server rejects it?

level: middleimportance: should knowfreq 40%

basics

~20 s

Apollo Client's optimisticResponse writes a guessed result into a separate optimistic cache layer, so the closed row repaints at once. When the server answers or the mutation fails, Apollo drops that layer, keeping the real result or rolling back.

open as a page

In Apollo Client 4, closeIssue returns { id status } and sets refetchQueries: ["OpenIssues"], yet the open-issues page opened next still lists the closed issue; why, and how do you fix it?

level: seniorimportance: should knowfreq 33%

basics

~20 s

The payload updates Issue:42's fields, not the cached issues(status: OPEN) list that references it, and refetchQueries by name refetches only watched queries, so the unmounted OpenIssues is skipped. Remove the issue from that cached list in an update function.

open as a page

Upgrading an issue-detail component from Apollo Client 3 to 4, what must change in a useQuery call that imports from @apollo/client and uses skip and onCompleted?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

In Apollo Client 4, import useQuery from @apollo/client/react and move onCompleted logic into render or an effect on data, because query hooks lost that callback. Replace skip with skipToken, deprecated since 4.3, and expect loading during refetch.

open as a page