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?
answer
- a function, not an object
- no argument reads, one argument writes
- lives beside the cache, not in it
- a hook subscribes the component
- pass a new value, never mutate
basics
~20 sA 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 linesimport { 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
Recall the shape: makeVar returns a function, no argument reads, one argument writes, and useReactiveVar makes a component re-render on change.
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.
Show where reactive variables stop helping: no persistence, no reset with the cache on logout, and no structure once many modules write them.
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.