In Apollo Client 4, how do you attach a short-lived access token to every dashboard request with SetContextLink, and what changed from setContext?
answer
- a link that edits context
- HttpLink reads context headers
- read the token per request
- spread the previous headers
- prevContext is now the first argument
basics
~20 sPlace 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 linesimport { 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
Recall that SetContextLink adds headers to the operation's context before HttpLink sends it, and that the token is read on every request.
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.
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.
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.