skip to content

In Apollo Client 4, how do you attach a short-lived access token to every dashboard request with SetContextLink, and what changed from setContext?

level: middleimportance: should knowfreq 38%

answer

  1. a link that edits context
  2. HttpLink reads context headers
  3. read the token per request
  4. spread the previous headers
  5. prevContext is now the first argument

basics

~20 s

Place a SetContextLink before HttpLink whose setter reads the current token and returns headers that spread prevContext.headers and add authorization. Apollo Client 4 replaced the deprecated setContext helper with this class and swapped the arguments to (prevContext, operation).

solid answer

~50 s

`HttpLink` builds its request headers from the operation's context, so auth is a link that writes `headers` into that context before `HttpLink` runs. `new SetContextLink((prevContext, operation) => ...)` calls its setter for every operation. The setter may be `async`, so it can await a token lookup, and whatever it returns is shallow-merged into the context. Two details matter. You must spread `prevContext.headers`, because the merge replaces the whole `headers` object and would drop headers set earlier. And you read the token inside the setter, so a token that rotates every few minutes is picked up without rebuilding the client. In Apollo Client 4, `setContext` is a deprecated wrapper whose callback took `(operation, prevContext)`; the class takes them the other way round. The client-level `headers` option is gone too, so static headers go on `HttpLink` instead.

code

ts · 22 lines
ts
import { ApolloClient, ApolloLink, HttpLink, InMemoryCache } from "@apollo/client";
import { SetContextLink } from "@apollo/client/link/context";

declare function getAccessToken(): Promise<string | null>;

const authLink = new SetContextLink(async (prevContext) => {
  const token = await getAccessToken();
  return {
    headers: {
      ...prevContext.headers,
      ...(token ? { authorization: `Bearer ${token}` } : {}),
    },
  };
});

export const client = new ApolloClient({
  link: ApolloLink.from([
    authLink,
    new HttpLink({ uri: "/graphql", headers: { "x-app": "analytics-dashboard" } }),
  ]),
  cache: new InMemoryCache(),
});

go deeper

for a junior

Recall that SetContextLink adds headers to the operation's context before HttpLink sends it, and that the token is read on every request.

for a middle

Explain the setter signature (prevContext, operation), the shallow merge that forces you to spread prevContext.headers, async token lookups, and the argument swap from the deprecated setContext.

for a senior

Show where the link must sit relative to RetryLink and a refreshing ErrorLink so that replays and retries re-read the token, and how you would spot a stale-token bug from symptoms.

for a principal

Argue for one auth link owned with the auth module rather than per-feature header code, so that token handling, opt-outs and header policy change in one reviewed place.

## Why auth is a link An Apollo Client 4 operation carries a **context**: a plain object that links read and write as the operation moves down the chain. `HttpLink`, the terminating link that performs the `fetch`, reads `headers` (as well as `uri`, `credentials` and `fetchOptions`) from that context and merges them with the static `headers` it was constructed with. Adding an `Authorization` header is therefore a matter of putting it into the context **before** `HttpLink` runs, and `SetContextLink` is the link built for exactly that. For an internal analytics dashboard behind an API gateway, every query that loads a panel must carry the user's current access token. The tokens are short-lived, so the value changes while the app is open. ## How SetContextLink works `new SetContextLink(setter)` runs `setter(prevContext, operation)` once per operation: - `prevContext` is a read-only snapshot of the context so far, including headers set by earlier links or passed through a hook's `context` option; - `operation` is the operation without its `getContext`/`setContext` methods, so the setter can branch on `operation.operationName` or `operation.variables`; - the return value, or the value of the Promise it returns, is **shallow-merged** into the context, and then the link calls `forward`. Because the setter can be `async`, it can await a token store that refreshes itself. Because it runs per operation, it always sends the latest token. Reading the token once at startup and baking it into `HttpLink`'s static `headers` is the classic bug: every request after the first expiry fails. ## The two details that break it 1. **Spread the previous headers.** The merge is shallow. Returning `{ headers: { authorization } }` replaces the whole `headers` object, so any context headers added upstream are lost, such as a tracing header or a per-query header passed through `useQuery`'s `context` option. Always return `{ headers: { ...prevContext.headers, authorization } }`. 2. **Put it before HttpLink.** A `SetContextLink` placed after the terminating link never runs, and the request goes out without the header. ## What changed from Apollo Client 3 | Apollo Client 3 | Apollo Client 4 | |---|---| | `setContext((operation, prevContext) => ...)` | `new SetContextLink((prevContext, operation) => ...)` | | `setContext` is the documented API | `setContext` still ships but is **deprecated** | | `new ApolloClient({ uri, headers })` | client requires `link`, and static headers go on `HttpLink` | The **argument order is swapped**. Code ported mechanically from 3.x that renames `setContext` to `SetContextLink` but keeps `(operation, prev)` will read `headers` from the operation, which has none, and lose the real ones. TypeScript usually catches it, and plain JavaScript does not. The deprecated `setContext` exists precisely as a shim that flips the arguments and constructs a `SetContextLink`. ## Where it sits relative to other links - **After** a token-refreshing `ErrorLink`, so that when that link replays an operation with `forward(operation)`, the replay passes through `SetContextLink` again and picks up the new token. - **After** `RetryLink`, for the same reason: every retry re-runs the links downstream of `RetryLink`, so each attempt reads the current token. - **Before** `HttpLink`, always. ## Diagnosing a stale-token bug The symptom is distinctive: the dashboard works right after sign-in or a reload, then every panel fails once the first token expires, and a reload fixes it again. To confirm: - open the network panel and compare the `authorization` header across requests; if it never changes, the token is being captured once; - look for a setter that closes over a variable assigned at startup instead of calling the token store; - check for a second client, created in a test helper or a provider, that bypasses the auth link entirely. The fix is always the same: read the token inside the setter, on every operation. ## Where the token comes from This leaf covers only where the header is attached. How tokens are issued, validated or refreshed is an authentication-protocol question. From the link's point of view, the setter calls "give me the current token", and the auth module owns what that means. A common pattern caches the token in memory and has the setter await a refresh only when the cached token is missing or about to expire.

  • Why not pass the token once through HttpLink's headers option in an Apollo Client 4 dashboard?
    `HttpLink`'s `headers` are fixed when the link is constructed. A short-lived token read at startup keeps being sent after it expires, and every request fails until the page reloads. A `SetContextLink` setter runs for each operation, so it sends whatever token is current at request time. Static values that never change, like an app identifier header, are fine on `HttpLink`.
  • How would one dashboard query skip the auth header in Apollo Client 4 without a second client?
    Have the setter branch on the operation. For example, it can return `prevContext` unchanged when `operation.operationName` is a public health-check query. Alternatively, pass a flag through the hook's `context` option and check it on `prevContext`. Because the setter sees both the operation and the incoming context, per-operation rules live in one link instead of in a separate client.

saying these in an interview costs you the question

  • Read the token once at startup and pass it in HttpLink's headers option.
  • SetContextLink deep-merges headers, so there is no need to spread prevContext.headers.
  • The SetContextLink callback takes (operation, prevContext), the same order as setContext.
  • Put SetContextLink after HttpLink so the header is added once the request is built.
  • In Apollo Client 4 you can still pass headers directly to the ApolloClient constructor.