skip to content

Subscriptions

Real-time GraphQL over WebSockets: setting up the link, splitting HTTP and WS traffic, and merging incoming events into the cache with subscribeToMore. Teardown and reconnection are the parts interviewers push on, because that is where subscriptions leak.

on this pageshow

explore

questions

6

In Apollo Client 4, what does the useSubscription hook give a notification badge, and when does the badge re-render?

level: juniorimportance: must knowfreq 38%

answer

  1. a long-lived operation, not a request
  2. loading until the first event
  3. latest event only, no history
  4. a re-render as each event lands
  5. onData for per-event side effects

basics

~20 s

useSubscription, imported from @apollo/client/react, returns loading, data, error and restart. loading stays true until the first event arrives; after that, data holds only the latest event, and the component re-renders as each pushed event lands.

solid answer

~50 s

`useSubscription(NOTIFICATION_ADDED, options)` from `@apollo/client/react` starts the subscription when the component mounts and returns `{ loading, data, error, restart }`. A subscription has no immediate response, so `loading` stays `true` until the server pushes the first event; a badge should show its last known count then, not a spinner. Each event replaces `data` and re-renders the component, so `data` is the latest event, never a history of past ones. For work that must happen once per event, such as bumping a counter or playing a chime, use the `onData({ client, data })` callback, which Apollo calls before the re-render, instead of a `useEffect` on `data` that can miss an event when renders are batched. `error` is set and `onError` called when an event carries errors or the transport fails. Passing `skipToken` instead of options defers the subscription, and unmounting ends it.

code

tsx · 26 lines
tsx
import { gql } from "@apollo/client";
import { useSubscription } from "@apollo/client/react";

const NOTIFICATION_ADDED = gql`
  subscription OnNotificationAdded {
    notificationAdded {
      id
      title
      unreadCount
    }
  }
`;

export function NotificationBadge({ initialCount, onNew }: {
  initialCount: number;
  onNew: (title: string) => void;
}) {
  const { data } = useSubscription(NOTIFICATION_ADDED, {
    onData({ data }) {
      if (data.data) onNew(data.data.notificationAdded.title);
    },
  });

  const count = data?.notificationAdded.unreadCount ?? initialCount;
  return <span aria-label={`${count} unread`}>{count}</span>;
}

go deeper

for a junior

Recall the four result fields, that loading means no event has arrived yet, and that data is only the latest event. Name onData as the per-event callback.

for a middle

Explain the render cycle per event, why a useEffect on data can miss events when renders batch, and how variables changes and skipToken start or stop the subscription.

for a senior

Show you know the edges: deduplicated subscriptions missing an initial value, the default cache write of each event, ignoreResults for render control, and restart after a transport failure.

for a principal

Frame when a hook-scoped subscription is the right owner for a live feed versus one app-level subscription that writes into the cache for many components to read.

## What a subscription is, seen from the hook A GraphQL **subscription** is a long-lived operation: the client sends it once, and the server pushes a result each time something happens, such as a new notification for the signed-in user. In Apollo Client 4 the React entry point for it is **`useSubscription`**, imported from `@apollo/client/react` (the core `@apollo/client` package has no React exports in version 4). The hook opens the operation when the component mounts, keeps it open while the component stays mounted, and ends it when the component unmounts. The transport underneath, a WebSocket link or multipart HTTP, is configured on the client, not in the hook. ## What the hook returns The result object has four fields: | Field | Before the first event | After an event | After an error | |---|---|---|---| | `loading` | `true` | `false` | `false` | | `data` | `undefined` | the latest event's payload | `undefined` with the default error policy | | `error` | `undefined` | `undefined` | the error, an `ErrorLike` | | `restart` | a function | a function | a function | Two points trip people up: - **`loading` means "no event yet", not "connecting".** It stays `true` until the server first pushes something, which may be minutes for a quiet notification feed. A badge should render its last known count, typically from a query, rather than a spinner. - **`data` is only the latest event.** The hook does not accumulate a list. Three pushed notifications leave `data` holding the third one; the earlier two are gone from the hook's result. `restart` disconnects and reconnects the subscription, which is useful after a transport failure. ## When the component re-renders 1. On mount the hook returns `loading: true` and subscribes. 2. The server pushes an event; Apollo stores it as the hook's current result, calls `onData` (or `onError` if the event has errors), and schedules a re-render. 3. The component renders with the new `data`. 4. Steps 2 and 3 repeat for every event until unmount, a `skipToken`, or the server completing the subscription (which calls `onComplete`). With `ignoreResults: true` the hook stops re-rendering the component entirely and you handle events only in the callbacks; switching it on while data is present resets `data` to `undefined`. ## Reacting to each event: onData, not useEffect A common first attempt watches `data` in a `useEffect` and increments a counter. React may batch two quick events into one render, and the effect then sees only the second, so an event is lost. `onData` runs for **every** event that arrives without errors (error events go to `onError`), before the re-render: ```tsx useSubscription(NOTIFICATION_ADDED, { onData({ data }) { // data is { loading, data, error }; data.data is the event payload if (data.data) setUnread((n) => n + 1); }, }); ``` The callback receives `{ client, data }`, where `data` is the result object, so the payload is `data.data`. ## Options worth knowing - **`variables`**: the subscription's arguments. When they change, the hook ends the old subscription and starts a new one; `shouldResubscribe` can veto that. - **`skipToken`**: pass it instead of the options object to hold the subscription until a required variable exists. In 4.3 the `skip` option is deprecated in its favour. - **`fetchPolicy`**: by default each event is also written to the normalized cache, so an object with `__typename` and `id` updates wherever else it is displayed; `"no-cache"` skips that write. - **`errorPolicy`**: `"none"` by default, which drops `data` when an event carries errors. - **`onError`** and **`onComplete`**: called for an error event and when the subscription ends, for example because the server completed it. - **`context`**: passed to the link chain, for example `{ queryDeduplication: false }`. ## Where the badge's number should come from A badge rarely lives on the subscription alone: - A **query** loads the starting count when the page opens, because the subscription only reports what happens after it starts. - The **subscription** pushes each change from then on. - If the event returns an object the cache can identify, such as the viewer with `__typename`, `id` and `unreadCount`, the default cache write updates that object, and every component that queried it re-renders with the new count without reading the hook's `data` at all. - If the event returns only the new notification, the badge takes the count from the hook's `data` or from `onData`, as in the example below. ## What changed in Apollo Client 4 - Hooks import from `@apollo/client/react`. - `onSubscriptionData` and `onSubscriptionComplete` were removed in 4.0; the callbacks are `onData` and `onComplete`. - The result no longer contains `variables`. - Subscriptions are deduplicated like queries: two components subscribing with the same document and variables share one connection. - `skipToken` works with `useSubscription` from 4.3.

  • A badge and an inbox list both call useSubscription with the same Apollo Client 4 subscription and variables; what happens?
    Apollo Client 4 deduplicates subscriptions under `queryDeduplication`, which is on by default, so the second hook attaches to the first one's connection instead of opening another. Both receive every later event. If the server sends an initial value on connect, only the first subscriber sees it, and the second waits for the next event. `context: { queryDeduplication: false }` forces a separate connection.
  • How do you keep an Apollo Client 4 useSubscription from starting until the signed-in user's id is known?
    Pass `skipToken` in place of the options: `useSubscription(DOC, userId ? { variables: { userId } } : skipToken)`. No operation is sent while it is skipped and `loading` is `false`; once real options arrive the hook subscribes. Since 4.3 this is the supported form, and the `skip: true` option is deprecated.
  • When would you set ignoreResults: true on an Apollo Client 4 useSubscription?
    When the component should not re-render for events at all and you handle them yourself in `onData` and `onError`, for example writing each notification into a list the parent renders. The hook then returns no `data`, and turning the option on while data is present resets `data` to `undefined`.

saying these in an interview costs you the question

  • useSubscription's data accumulates every event the server has pushed so far.
  • loading turns false as soon as the WebSocket connection opens.
  • A useEffect watching data is the reliable way to handle every event.
  • In Apollo Client 4 the per-event callback is onSubscriptionData.
  • useSubscription is imported from @apollo/client in Apollo Client 4.
  • The subscription keeps running after its component unmounts.
open as a page

In Apollo Client 4, how does subscribeToMore add a newly pushed support-chat message to a room's already-loaded message list?

level: middleimportance: should knowfreq 30%

basics

~20 s

subscribeToMore, returned by useQuery, starts a subscription tied to that query. For each event its updateQuery callback gets the query's cached result and the pushed message, returns a new result with the message appended, and Apollo writes that back to the cache.

open as a page

Why does an Apollo Client 4 support chat using subscribeToMore show the previous room's messages, some twice, after an agent switches rooms?

level: seniorimportance: should knowfreq 22%

basics

~20 s

The effect that calls subscribeToMore never returns its unsubscribe function, so each room's subscription outlives the switch. useQuery keeps one query with new variables, so old subscriptions write into the current room, and two live subscriptions on one room append each message twice.

open as a page

With Apollo Client 4's GraphQLWsLink, how do you authenticate the support-chat socket and keep messages flowing when the token expires or the socket drops?

level: seniorimportance: should knowfreq 20%

basics

~20 s

GraphQLWsLink ignores context headers, so SetContextLink auth never reaches it; send the token in graphql-ws connectionParams, read at each connection. graphql-ws retries drops itself; once it gives up, useSubscription reports a socket-closed error and completes, so restart and refetch.

open as a page

When would you run Apollo Client 4 subscriptions as multipart HTTP through HttpLink instead of a GraphQLWsLink WebSocket, and what does it cost?

level: seniorimportance: nice to knowfreq 12%

basics

~20 s

When the server supports multipart subscriptions, HttpLink streams them with no extra library, split or connectionParams, reusing the HTTP auth headers. The cost is one long-lived HTTP response per subscription, a one-way channel, and a server that must implement the protocol.

open as a page