# GraphQL Clients (Apollo)

> Consuming GraphQL from the front end: how Apollo Client's normalized cache, queries, and mutations change data fetching versus REST calls.


---

# GraphQL Clients (Apollo)

You can write a GraphQL query in five minutes. Then you wire it into a real app and the questions start: where does the response live, why does one component update when another runs a mutation, and how do you stop the same data being fetched four times? A raw `fetch` won't answer any of that. Apollo Client does, and this guide is about the model that makes it work.

The relief here is that once you understand Apollo's **normalized cache**, most of the busywork of front-end data fetching disappears. You stop hand-managing loading flags, stale lists, and duplicated requests, because the cache does it for you. The cost is a new mental model you have to actually hold in your head, and a few new ways to get burned.

## How to read this

Read the phases in order. Phase 1 builds the mental model: what Apollo Client is, why a cache sits at its center, and how that differs from thinking in REST endpoints. Phase 2 is the everyday work: writing queries and mutations with hooks, and keeping the cache accurate after a write. Phase 3 is production reality: the gotchas that bite, from cache misses to refetch storms.

If you've never written a GraphQL query at all, skim [GraphQL Explained](/guides/graphql-explained) first, then come back. If REST is your only reference point, [REST APIs Explained](/guides/rest-apis-explained) gives you the contrast this guide keeps drawing on.

## The phases

1. [The cache is the point](01-the-cache-is-the-point.md) - what Apollo Client actually is, and why a normalized cache changes everything
2. [Queries and mutations in real components](02-queries-and-mutations.md) - the hooks you use daily, and updating the cache after a write
3. [When the cache lies to you](03-production-reality.md) - cache misses, refetch storms, and the tradeoffs you signed up for


---

# The cache is the point

Here's the trap most people fall into. They learn GraphQL, see that it returns JSON over HTTP, and conclude that a GraphQL client is "fetch, but for GraphQL." So they reach for `fetch`, POST a query string, and get JSON back. That works for one screen. Then the app grows, and the parts that have nothing to do with GraphQL start hurting: the same user is loaded by three components, a profile edit doesn't update the header, and every list keeps a stale copy of rows you already have.

Apollo Client exists to solve that second set of problems. The query syntax is the small part. The big part is a **normalized cache** that sits between your components and the network, and the whole reason to pull in a client library instead of `fetch` is that cache.

## What GraphQL gives the client first

Before the cache, one thing about GraphQL itself shapes everything that follows: the client asks for exactly the fields it wants. With REST, the endpoint decides the shape of the response. With GraphQL, your component does.

```graphql
query GetUser {
  user(id: "42") {
    id
    name
    avatarUrl
  }
}
```

*What just happened:* you asked for three fields and you'll get exactly those three back - no `email`, no `createdAt`, no nested objects you didn't request. Under REST this is the over-fetching problem (the endpoint sends a fat object) and the under-fetching problem (you need a second call for related data). GraphQL collapses both: one request, the precise shape, related data nested in.

If that contrast is new to you, the deep version lives in [GraphQL Explained](/guides/graphql-explained) and [REST APIs Explained](/guides/rest-apis-explained). Here we take it as given and ask the next question: where does that response go once it arrives?

## The normalized cache, plainly

A naive client would store responses by query. "The result of `GetUser` is *this blob*." That's what a `fetch`-based setup does, and it's why the same user ends up duplicated - every query that mentions user 42 keeps its own copy, and they drift apart.

Apollo does something different. It **flattens** every response into a flat table of objects, keyed by type and id. User 42 is stored once, under a key like `User:42`, no matter how many queries returned it.

```text
ROOT_QUERY
  user({"id":"42"})  ──►  ref: User:42

User:42
  id:        "42"
  name:      "Ada"
  avatarUrl: "https://.../ada.png"
```

*What just happened:* the cache split the response into two pieces. `ROOT_QUERY` remembers that the `user(id: 42)` query points at the object `User:42`, and `User:42` holds the actual fields. The query result is a *reference*, not a copy. That single indirection is the whole trick.

Why does that matter? Because the next time *any* query returns user 42 - a different screen, a different query name, a list that happens to include them - Apollo writes the fields back into the same `User:42` entry. Every component reading that user is looking at one source of truth.

```text
ROOT_QUERY
  user({"id":"42"})       ──►  ref: User:42
  teamMembers             ──►  [ ref: User:42, ref: User:51, ... ]

User:42   (one entry, two readers)
  name: "Ada"   ◄── update this once, both views re-render
```

*What just happened:* a list query and a single-user query both resolved to the same `User:42` object. Update that name once and Apollo re-renders every component subscribed to it. No event bus, no manual prop-drilling, no "refresh the header after save." This is the payoff you cannot get from `fetch` without rebuilding a chunk of Apollo by hand.

> The cache keys off `__typename` plus `id` (or `_id`) by default. If your objects don't carry a stable id, normalization silently can't happen and you lose most of this benefit. We come back to that landmine in Phase 3.

## How the pieces fit

```mermaid
flowchart LR
  C[Component] -->|useQuery| A[Apollo Client]
  A -->|cache hit| K[(Normalized cache)]
  A -->|cache miss| N[GraphQL server]
  N --> K
  K --> C
```

*What just happened:* a component asks for data through a hook. Apollo checks the normalized cache first. On a hit, it returns instantly with no network call. On a miss, it goes to the server, writes the result into the cache, and serves it. The component never talks to the network directly - it talks to the cache, and the cache talks to the network.

This is the mental flip from REST. In a REST app you think in *requests*: "call `GET /users/42`, hold the result in component state." In Apollo you think in *data that already lives somewhere*: "I need user 42; give me whatever the cache knows, fetch only if it's missing." The request becomes an implementation detail of the cache.

## What you give up

None of this is free, and pretending otherwise is how people get burned in Phase 3. The normalized cache is a second copy of your server's data living in the browser, and like any cache it can be wrong. After a mutation, after a delete, after another user changes something - the cache can hold data the server no longer agrees with. Most of the real work with Apollo is keeping that cache accurate, which is exactly what Phase 2 is about.

**For builders:** the practical line is this. If your app shows the same entities across many screens and edits them in place, the normalized cache earns its weight fast. If you're building a few read-only pages that never share data, `fetch` plus a query string is genuinely enough, and reaching for Apollo is the kind of complexity you'll resent later.

```quiz
[
  {
    "q": "What is the main reason to use Apollo Client instead of plain fetch for GraphQL?",
    "choices": [
      "It makes GraphQL queries shorter to write",
      "Its normalized cache stores each entity once and keeps all views in sync",
      "It is the only way to send a GraphQL query over HTTP",
      "It converts GraphQL responses into REST endpoints"
    ],
    "answer": 1,
    "explain": "GraphQL works fine over fetch. The value Apollo adds is the normalized cache: one copy per entity, automatic view updates."
  },
  {
    "q": "How does Apollo's normalized cache store a response?",
    "choices": [
      "As one blob keyed by the query name",
      "As flat objects keyed by type and id, with queries holding references to them",
      "As raw HTTP responses on disk",
      "It does not store responses; it refetches every time"
    ],
    "answer": 1,
    "explain": "Responses are flattened into a table keyed by __typename and id; query results become references into that table, so an entity is stored once."
  },
  {
    "q": "By default, what does Apollo use to identify an object for normalization?",
    "choices": [
      "The query name",
      "The HTTP status code",
      "The object's __typename combined with its id (or _id)",
      "The order it appeared in the response"
    ],
    "answer": 2,
    "explain": "Default cache keys are __typename + id/_id. Without a stable id, normalization cannot happen and the cache benefits mostly vanish."
  }
]
```


---

# Queries and mutations in real components

Phase 1 was the model. Now the daily work. In practice you spend your time in three places: reading data with a query hook, changing it with a mutation hook, and reusing field selections with fragments. The thing that ties them together is still the cache - every one of these tools is really about reading from or writing to it.

## Reading with useQuery

The `useQuery` hook is your default for "this screen needs this data." You give it a query, it gives you back three things you'll use constantly: `loading`, `error`, and `data`.

```jsx
const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      avatarUrl
    }
  }
`;

function Profile({ id }) {
  const { loading, error, data } = useQuery(GET_USER, {
    variables: { id },
  });

  if (loading) return <Spinner />;
  if (error) return <ErrorBox error={error} />;
  return <h1>{data.user.name}</h1>;
}
```

*What just happened:* on first render `loading` is `true` and Apollo fires the request. When it resolves, the result lands in the normalized cache and the hook re-renders with `data` filled in. Mount this same component again with the same `id` and `loading` may never flip to `true` at all - the cache already has `User:42`, so it serves instantly. That "instant on the second visit" is the cache working, not magic.

Notice what you did *not* write: no `useState` for the result, no `useEffect` to fetch, no manual cleanup. In a REST app you'd hand-roll all three. Here the hook owns the lifecycle and the cache owns the data.

## Writing with useMutation

Mutations change server data. The hook hands you a function to call and the same status fields for the in-flight write.

```jsx
const RENAME_USER = gql`
  mutation RenameUser($id: ID!, $name: String!) {
    renameUser(id: $id, name: $name) {
      id
      name
    }
  }
`;

function RenameButton({ id }) {
  const [renameUser, { loading }] = useMutation(RENAME_USER);

  return (
    <button
      disabled={loading}
      onClick={() => renameUser({ variables: { id, name: "Grace" } })}
    >
      Rename
    </button>
  );
}
```

*What just happened:* clicking sends the mutation. The server responds with the updated user, and because that response includes `id` and the changed field, Apollo writes it straight back into `User:42`. Every component reading that user - the header, a sidebar, a list row - re-renders with the new name. You wrote zero update logic, and the whole app stayed consistent.

That automatic write-back is the happy path, and it only works because you asked the mutation to **return the changed entity with its id**. Drop the `id` from the selection and Apollo can't match it to a cache entry, so nothing updates. Returning the fields you changed, with the id, is the single most important mutation habit.

## When the cache can't update itself

Apollo updates the cache for free in exactly one case: a mutation that modifies an **existing** entity and returns it. It cannot guess for the two cases it has no way to reason about - **adding** to a list and **removing** from one. There's no rule that says "a new comment belongs in *that* query's results," so you have to tell it.

The clean tool for adding is the mutation's `update` function, which hands you the cache and the mutation result.

```jsx
const ADD_COMMENT = gql`
  mutation AddComment($postId: ID!, $text: String!) {
    addComment(postId: $postId, text: $text) {
      id
      text
    }
  }
`;

useMutation(ADD_COMMENT, {
  update(cache, { data: { addComment } }) {
    cache.modify({
      id: cache.identify({ __typename: "Post", id: postId }),
      fields: {
        comments(existing = []) {
          const ref = cache.writeFragment({
            data: addComment,
            fragment: gql`fragment NewComment on Comment { id text }`,
          });
          return [...existing, ref];
        },
      },
    });
  },
});
```

*What just happened:* after the comment is created, the `update` function reaches into the post's `comments` field and appends a reference to the new comment. `cache.modify` targets one field on one entity; `writeFragment` puts the new object into the cache and hands back a reference to it. The list re-renders with the new row, no refetch required.

For a delete, the symmetric move is `cache.evict` to drop the entity, then `cache.gc()` to clean up references. The principle holds: **structural changes to lists are yours to make; in-place field edits are Apollo's.**

> If hand-writing cache updates feels heavy for a given mutation, the escape hatch is `refetchQueries` - name the queries to re-run after the write and let the server be the source of truth. It costs a round trip, but it's correct and readable. Reach for surgical cache updates only where the extra request actually hurts.

## Fragments: stop repeating field lists

Once several components read the same entity, they tend to ask for the same fields. A **fragment** is a named, reusable selection you drop into multiple queries.

```graphql
fragment UserCard on User {
  id
  name
  avatarUrl
}

query GetTeam {
  team {
    id
    members {
      ...UserCard
    }
  }
}
```

*What just happened:* `UserCard` defines the fields a user card needs once, and any query spreads it with `...UserCard`. Beyond saving typing, fragments keep a component's data needs next to the component itself - the card declares what it reads, and every query that renders a card pulls in exactly those fields. It also keeps cache entries consistent: when many queries select the same fields via one fragment, they all fill the same slots on `User:42`.

**In the wild:** teams co-locate a fragment with the component that uses it, then compose page-level queries out of those fragments. The page query becomes a list of `...ComponentFragment` spreads, and each component owns its own data contract. It's the GraphQL equivalent of a component declaring its props.

```quiz
[
  {
    "q": "After a mutation that edits an existing entity, why does Apollo update all views automatically?",
    "choices": [
      "It re-runs every active query on the page",
      "The response includes the entity's id, so Apollo writes the fields back to that cache entry",
      "It polls the server until the data matches",
      "It clears the entire cache and refetches"
    ],
    "answer": 1,
    "explain": "When a mutation returns the changed entity with its id, Apollo merges it into the matching cache entry, and every subscriber re-renders."
  },
  {
    "q": "Which change does Apollo NOT handle automatically after a mutation?",
    "choices": [
      "Updating a field on an existing entity that the mutation returned",
      "Adding a new item to a list or removing one from it",
      "Re-rendering components that read a changed entity",
      "Storing the returned entity under its type and id"
    ],
    "answer": 1,
    "explain": "Apollo can't know which lists a new or deleted item belongs to, so adds and removes need an update function (or refetchQueries)."
  },
  {
    "q": "What is the main purpose of a GraphQL fragment in an Apollo app?",
    "choices": [
      "To split a query across multiple network requests",
      "To define a reusable, named set of fields that components share",
      "To disable the normalized cache for certain fields",
      "To convert a query into a mutation"
    ],
    "answer": 1,
    "explain": "A fragment is a named selection of fields, letting components declare their data needs and share consistent selections across queries."
  }
]
```


---

# When the cache lies to you

Everything good about Apollo comes from the cache, and so does everything that goes wrong. The failures in this phase are not bugs in Apollo - they're the predictable consequences of keeping a second copy of your data in the browser. Once you can name them, they stop being mysteries and become a short checklist.

## The missing id, the silent killer

This is the one that costs people the most hours. Apollo normalizes by `__typename` plus `id`. If a query forgets to select `id`, or a type genuinely has no id, Apollo can't build a cache key - so it stores that object *inside* the query result instead of in the flat table. Normalization silently doesn't happen, and the symptoms look like everything else.

```graphql
query GetUser {
  user(id: "42") {
    name        # no id selected!
    avatarUrl
  }
}
```

*What just happened:* Apollo received a user but had no id to key it on, so it stashed the object under the query rather than as `User:42`. Now a mutation that updates `User:42` elsewhere has nothing to match against here, this view goes stale, and the same user gets duplicated across queries. The fix is boring and absolute: **select `id` in every selection of every normalizable type.** When updates aren't propagating, check for a missing id before you check anything else.

> You can inspect the actual cache with the Apollo Client Devtools browser extension. Open the cache tab and look for entries keyed `User:42` versus objects buried inside `ROOT_QUERY`. The buried ones are your un-normalized data, staring back at you.

## Fetch policies: how much do you trust the cache?

By default `useQuery` uses `cache-first`: if the data is in the cache, use it and skip the network entirely. That's fast, and it's exactly wrong for data that goes stale - a dashboard, an inbox, anything other people change. The lever is `fetchPolicy`.

```jsx
useQuery(GET_INBOX, { fetchPolicy: "cache-and-network" });
```

*What just happened:* `cache-and-network` shows the cached data instantly *and* fires a request to refresh it, updating the view when the response lands. The user sees something immediately and gets fresh data a moment later. The common policies are worth memorizing:

```text
cache-first       cache if present, else network   (default; fast, can be stale)
cache-and-network cache now, network always         (instant + fresh; extra request)
network-only      always network, then cache it     (fresh; no instant paint)
no-cache          always network, never store       (one-off, sensitive data)
```

*What just happened:* each policy is a different answer to "how much do you trust what's already in the cache?" There's no globally correct choice - pick per query based on how fast the underlying data changes. Treating every query as `cache-first` is how you ship a stale UI.

## The refetch storm

`refetchQueries` is the safe, readable way to keep the cache accurate after a mutation - but it's also how you accidentally hammer your server. Each named query you list is a fresh network request, and it's tempting to list "everything that might have changed" after every write.

```jsx
useMutation(ADD_TODO, {
  refetchQueries: ["GetTodos", "GetStats", "GetSidebar", "GetActivity"],
});
```

*What just happened:* every time someone adds a todo, four full queries re-run against the server. On a busy screen with several such mutations, you've turned one user action into a flurry of round trips. The fix is the targeted cache update from Phase 2 for hot paths, and reserving `refetchQueries` for the writes where a round trip is genuinely cheaper than hand-written cache surgery. Correctness first, but watch the request count in your network tab.

## Optimistic UI and its rollback

For snappy interactions you can tell Apollo to assume a mutation will succeed and update the cache *before* the server responds, via `optimisticResponse`. The view updates instantly; if the server later rejects the write, Apollo rolls the cache back to where it was.

```jsx
renameUser({
  variables: { id, name: "Grace" },
  optimisticResponse: {
    renameUser: { __typename: "User", id, name: "Grace" },
  },
});
```

*What just happened:* the UI showed "Grace" the instant the button was clicked, before any network round trip. If the mutation succeeds, the real response replaces the optimistic one seamlessly. If it fails, Apollo discards the optimistic write and the old name snaps back. The gotcha: your `optimisticResponse` must include `__typename` and `id`, or Apollo can't apply it to the right entry - the same id discipline as everywhere else.

## The real tradeoff

Step back and the shape of the deal is clear. REST gives you a simple mental model - a request, a response, state you hold yourself - and makes over- and under-fetching your problem. Apollo solves fetching shape and cross-screen consistency, and hands you a cache to keep accurate in return. You traded *"my data is stale because I forgot to refetch"* for *"my cache is wrong because I forgot to update it."* Different failure mode, not zero failure mode.

**For builders:** the teams who stay happy with Apollo treat the cache as a real part of their architecture, not an invisible convenience. They select `id` everywhere, choose fetch policies deliberately, write surgical cache updates on hot paths, and keep the Devtools cache tab open when something looks stale. Do that, and the normalized cache pays for itself many times over. Ignore it, and you'll spend Phase 3's gotchas in production instead of in this guide.

```quiz
[
  {
    "q": "Updates from a mutation aren't showing up in one component. What should you check first?",
    "choices": [
      "Whether the server is down",
      "Whether that component's query selected the entity's id",
      "Whether React is installed correctly",
      "Whether the network is offline"
    ],
    "answer": 1,
    "explain": "A missing id means Apollo can't normalize the object, so it never matches the cache entry a mutation updates. Check for id first."
  },
  {
    "q": "Which fetch policy shows cached data immediately and also fetches fresh data from the network?",
    "choices": [
      "cache-first",
      "no-cache",
      "cache-and-network",
      "network-only"
    ],
    "answer": 2,
    "explain": "cache-and-network paints from the cache instantly and fires a request to refresh, updating when it returns - instant plus fresh, at the cost of an extra request."
  },
  {
    "q": "What is the risk of listing many queries in refetchQueries after every mutation?",
    "choices": [
      "It disables the normalized cache permanently",
      "Each listed query is a fresh network request, so writes can trigger a storm of round trips",
      "It deletes the cache entries it refetches",
      "It converts the mutation into a query"
    ],
    "answer": 1,
    "explain": "refetchQueries re-runs each named query against the server. Listing many on hot mutations multiplies network traffic; use targeted cache updates instead."
  }
]
```
