skip to content

Local State and Reactive Vars

Apollo can hold client-only state too — reactive variables via makeVar, @client fields, and cache-only type policies — so a single query returns both local and remote data. Interviewers ask when this beats reaching for a separate state library.

on this pageshow

explore

questions

5

In Apollo Client 4, what is a reactive variable created with makeVar, and how would you use one for a shop's cart drawer open flag?

level: juniorimportance: must knowfreq 36%

answer

  1. a function, not an object
  2. no argument reads, one argument writes
  3. lives beside the cache, not in it
  4. a hook subscribes the component
  5. pass a new value, never mutate

basics

~20 s

A reactive variable is the function makeVar(initialValue) returns: call it with no argument to read, with one argument to write. It lives outside the cache, and components that read it through useReactiveVar re-render when it changes.

solid answer

~40 s

`makeVar(false)` returns a function, say `cartDrawerOpenVar`. Calling `cartDrawerOpenVar()` returns the current value and `cartDrawerOpenVar(true)` stores a new one, from anywhere: a click handler, a mutation callback, code outside React. The value is not stored in `InMemoryCache`, so it needs no `__typename`, no id and no GraphQL document, and it can hold any type. To make a component follow it, read it with `useReactiveVar(cartDrawerOpenVar)` from `@apollo/client/react`; a plain `cartDrawerOpenVar()` call in the render body reads the value once and does not subscribe. Active queries also update when a field's type-policy `read` function reads the variable. A write only notifies readers when the new value differs by `!==`, so pass a new array or object instead of mutating the old one. Nothing persists the value across a page reload.

code

tsx · 23 lines
tsx
import { makeVar } from "@apollo/client";
import { useReactiveVar } from "@apollo/client/react";

export const cartDrawerOpenVar = makeVar(false);

export function CartButton() {
  // Writes only: no hook, no re-render of the button
  return (
    <button onClick={() => cartDrawerOpenVar(!cartDrawerOpenVar())}>
      Cart
    </button>
  );
}

export function CartDrawer() {
  const open = useReactiveVar(cartDrawerOpenVar);
  if (!open) return null;
  return (
    <aside>
      <button onClick={() => cartDrawerOpenVar(false)}>Close</button>
    </aside>
  );
}

go deeper

for a junior

Recall the shape: makeVar returns a function, no argument reads, one argument writes, and useReactiveVar makes a component re-render on change.

for a middle

Explain why a direct call in the render body does not subscribe, and why mutating an array in place is invisible to the !== check in the setter.

for a senior

Show where reactive variables stop helping: no persistence, no reset with the cache on logout, and no structure once many modules write them.

for a principal

Frame reactive variables as cheap global UI state and say when that global, unstructured shape becomes a maintenance cost for a team.

## What a reactive variable is A **reactive variable** is Apollo Client's container for one piece of local state that does not come from the server. You create it with `makeVar`, which is exported from `@apollo/client` (the React hooks live in `@apollo/client/react`, but `makeVar` does not): ```ts import { makeVar } from "@apollo/client"; export const cartDrawerOpenVar = makeVar(false); ``` `makeVar` does not return an object with `get` and `set` methods. It returns a **function**, and the number of arguments you pass decides what the call does. The initial value can be anything: a boolean, a string, an array of product ids, a plain object. ## Reading and writing | Call | What it does | |---|---| | `cartDrawerOpenVar()` | returns the current value | | `cartDrawerOpenVar(true)` | stores `true` and notifies readers if the value changed | | `cartDrawerOpenVar.onNextChange(fn)` | calls `fn` once, on the next change, and returns an unsubscribe function | You can call the variable from anywhere: an event handler, a mutation callback, a router hook, plain code with no React in it. There is no GraphQL document, no `__typename` and no id involved. The setter compares the new value with the stored one using `!==`, which matters for arrays and objects: 1. `cartItemIdsVar().push(id)` mutates the stored array in place. The setter is never called, so no reader hears about it. 2. Calling `cartItemIdsVar(sameArray)` afterwards passes the same reference. The comparison sees no change, and still no reader is notified. 3. `cartItemIdsVar([...cartItemIdsVar(), id])` passes a new array, so the change is detected and broadcast. Treat the stored value as immutable and always pass a new one. ## Making React follow it Reading a reactive variable is not the same as subscribing to it. A change reaches the screen in two ways: - **`useReactiveVar(cartDrawerOpenVar)`** returns the current value and re-renders the component whenever the variable changes. Under the hood it registers an `onNextChange` listener through React's external-store subscription and re-registers it after every change. - **A query field whose type-policy `read` function reads the variable.** Apollo records that the field depends on the variable, recomputes it when the variable changes, and every active query that selects the field delivers a new result. A plain `cartDrawerOpenVar()` call in a component's render body reads the value once. It shows the right value on the first render and after any re-render caused by something else, which is why the bug hides: the drawer seems to open "sometimes", whenever an unrelated state change happens to re-render the component. A component that only **writes** the variable, such as the header's cart button, needs no hook. It can call `cartDrawerOpenVar(!cartDrawerOpenVar())` in its click handler without re-rendering itself; only the drawer, which reads the flag with `useReactiveVar`, re-renders. Several components may call `useReactiveVar` on the same variable; each one subscribes on its own, and each re-renders when the value changes. Components that neither read the variable through the hook nor select a field that depends on it are left alone, so a drawer flag toggled many times does not re-render the product grid. ## What a reactive variable is not - **Not stored in the cache.** It sits beside `InMemoryCache`, so it is not normalized, has no cache ID and cannot be changed with `cache.modify` or `cache.writeQuery`. - **Not reset with the cache.** Resetting the cache clears cached query data; a reactive variable keeps whatever value it last had. Code that logs a user out must set the variables it cares about back by hand. - **Not persisted.** It starts from its initial value on every page load. Apollo has no built-in persistence for reactive variables, so saving a theme choice means writing it to browser storage yourself. - **Not a GraphQL field by itself.** A query can only see it through a local-only field marked `@client` whose `read` function returns the variable, and in Apollo Client 4 `@client` fields also need `localState: new LocalState()` on the client. - **Not a structured store.** There are no actions, reducers or selectors; any module that imports the variable can overwrite it. ## When it is the right tool Reactive variables fit small, app-wide pieces of UI state in an app that already uses Apollo: a drawer flag, a theme, the ids in a cart. They are cheap to create, need no provider, and connect directly to queries through `read` functions when a local field should depend on them. They are a poor fit for large, interrelated client state with many writers, where a structured store earns its extra ceremony.

  • Why does cartItemIdsVar().push(id) change nothing on screen, while cartItemIdsVar([...cartItemIdsVar(), id]) works?
    The setter notifies readers only when the new value differs from the stored one by `!==`. `push` mutates the stored array in place without calling the setter, and even calling `cartItemIdsVar(sameArray)` afterwards passes the same reference, so no change is detected. Spreading into a new array gives a new reference, which the setter sees as a change and broadcasts.
  • A user picks the dark theme, stored in themeVar, and reloads the page. What do they see, and how do you keep the choice?
    They see the initial value again: a reactive variable is plain in-memory state that `makeVar` initialises on every load, and Apollo has no built-in persistence for it. Initialise it from storage, for example `makeVar(window.localStorage.getItem("theme") ?? "light")`, and write the value back in the same function that sets the variable.
  • How would you react to a reactive variable change outside React, for example to sync the theme to the page?
    Use `themeVar.onNextChange(listener)`. It fires once, on the next change, and returns an unsubscribe function, so a long-lived listener re-registers itself inside the callback. `useReactiveVar` does exactly that internally; outside React you write the loop yourself, or you set the page theme in the same function that writes the variable.

saying these in an interview costs you the question

  • A reactive variable is stored in InMemoryCache like any other cached field.
  • Calling cartDrawerOpenVar() in a component body re-renders it on every change.
  • Pushing into the array a reactive variable holds notifies every reader.
  • A reactive variable needs a __typename and an id before Apollo can track it.
  • makeVar is imported from @apollo/client/react together with the hooks.
  • Every component that writes a reactive variable must call useReactiveVar first.
open as a page

In Apollo Client 4, how do you add a locally computed isInCart field to a server Product queried alongside its name and price?

level: middleimportance: must knowfreq 32%

basics

~20 s

Select isInCart @client in the query, give Product.isInCart a read function in InMemoryCache typePolicies, and pass localState: new LocalState() to ApolloClient. Apollo strips the field from the request and fills it from the read function.

open as a page

An Apollo Client 3 shop passes local resolvers to new ApolloClient and reads cache from the resolver context; what changes when you move them to Apollo Client 4's LocalState?

level: seniorimportance: should knowfreq 22%

basics

~20 s

Resolvers move from the client's resolvers option into new LocalState({ resolvers }) from @apollo/client/local-state, passed as localState. The context becomes { requestContext, client, phase }, so cache is client.cache, and errors, undefined and missing __typename are handled strictly.

open as a page

A shop's React app already uses Apollo Client 4 for server data; when would you keep its cart drawer flag, theme and isInCart in Apollo rather than a separate state library?

level: principalimportance: should knowfreq 20%

basics

~20 s

Keep state in Apollo when it decorates server entities or a query must read it, like isInCart. Small UI flags like the drawer or theme can be reactive variables, but interrelated client state with many writers belongs in a dedicated store.

open as a page

In Apollo Client 4, an isInCart @client field computed by a LocalState resolver shows stale values after the cart changes, while a read-function version stays current; why?

level: seniorimportance: nice to knowfreq 16%

basics

~20 s

A LocalState resolver runs when the operation executes, and its output is cached with the server result, so cache-first reads return the stored value. A read function computes the field on read and tracks the reactive variables it reads.

open as a page