skip to content

For a React storefront team choosing between urql and Apollo Client 4, when is urql the better fit, and what does the team give up?

level: principalimportance: should knowfreq 20%

answer

  1. small core, features as exchanges
  2. start with the document cache
  3. normalize later, as an opt-in
  4. no built-in local state
  5. writes only as reactions

basics

~20 s

urql fits when a team wants a small, exchange-based client that starts with a simple document cache and adds Graphcache only when shared entities demand it. The team gives up built-in local state and Apollo's freedom to write to the cache anywhere.

solid answer

~50 s

urql suits a storefront that is mostly read-heavy catalogue pages: the default document cache needs no configuration, invalidates by `__typename` after mutations, and everything else, from auth to retries to a normalized cache, is an exchange you add to one array. When cart mutations and shared entities appear, you swap in Graphcache without touching components. Apollo Client 4 starts from a normalized `InMemoryCache` with type and field policies, offers local state through `@client` fields and reactive variables, and lets code write to the cache from anywhere. Choosing urql means keeping client-only state in React or a store, accepting that Graphcache only changes data inside updaters and resolvers, and planning the document-cache-to-Graphcache move up front. The deciding questions are how relational the data is, whether the team needs cache-backed local state, and whether it values a pipeline it can rearrange over a richer cache API.

go deeper

for a junior

Recall the headline difference: urql starts with a simple document cache and adds a normalized one by exchange, while Apollo Client starts normalized.

for a middle

Explain what each library's default cache does after a mutation and where each keeps client-only state.

for a senior

Compare the cost of moving from urql's document cache to Graphcache with the cost of configuring Apollo's type policies, using a concrete screen.

for a principal

Own the decision: map shared entities, local-state needs and extension needs, name the migration you are signing up for, and prototype the hardest screen before committing.

## What the choice is really about Both libraries send GraphQL operations, cache results and expose React hooks. The difference is in **where complexity lives by default**. urql starts with the least machinery and makes you opt in; Apollo Client starts with a normalized cache and a larger API surface. For a lead, the question is which default matches the product's data and the team's habits, and how expensive it is to change course later. ## Where urql fits - **A small, rearrangeable core.** Every behaviour, including caching, is an **exchange** in the client's `exchanges` array. Authentication, retries, persisted documents and the normalized cache are separate exchanges you add or swap, so the data layer changes by editing one array. - **A cache you can start without configuring.** The default **document cache** stores one result per query and refreshes queries after a mutation by matching `__typename`. For catalogue pages that are read far more than written, that is often all you need. - **A migration path to normalization.** Graphcache, from `@urql/exchange-graphcache`, replaces the default cache when entities are shared across screens and mutations get frequent. Components keep calling `useQuery` and `useMutation`; only the client configuration changes. - **Bindings beyond React.** The same core powers official packages for Preact, Vue, Svelte and Solid, which matters for a team with more than one front end. ## What the team gives up | Concern | urql 5 with Graphcache 9 | Apollo Client 4 | |---|---|---| | Default cache | document cache; normalized cache is opt-in | normalized `InMemoryCache`, passed as the required `cache` option | | Local, client-only state | not provided; keep it in React state or a store | `@client` fields, `makeVar` reactive variables, local resolvers via `LocalState` | | Writing to the cache | Graphcache writes only inside `updates`, `resolvers` and `optimistic` | `cache.writeQuery`, `cache.modify` and friends, callable where code has the cache | | Per-field cache behaviour | Graphcache `keys`, `resolvers`, `updates` | `typePolicies` with `keyFields` and field `read`/`merge` functions | | Extension point | exchanges, which can also replace caching | links, which sit on the network side | Each row is a real cost for some teams: - A product that keeps UI state in the GraphQL cache, such as a cart drawer's open flag read alongside server fields, has no direct urql equivalent. - A team used to patching the cache imperatively from event handlers must move that logic into updaters, where Graphcache runs it as a reaction to a mutation or subscription result. - Starting on the document cache means the invalidation model changes once, at the switch to Graphcache; screens that relied on `additionalTypenames` tags get retested. ## Signals that the choice needs revisiting - The list of `additionalTypenames` tags keeps growing, and screens still show stale carts. - Updaters in `updates.Mutation` repeat the same list edits for many mutations, which usually means mutations should return their parent objects. - The team keeps reaching for a place to put client-only state next to server data. - Product pages refetch after every cart change because they share the `Product` type with mutation results. ## A decision path 1. **Map the data.** List the entities that appear on more than one screen and the mutations that change them. Few shared entities and rare writes favour urql's document cache; a dense graph of shared entities favours a normalized cache from day one, in either library. 2. **Decide where UI state lives.** If the team wants client-only state in the same cache as server data, Apollo supports that directly. If it already uses React state or a store, urql loses nothing here. 3. **Look at the extension needs.** If the team expects to change fetching, auth or caching behaviour often, urql's exchange pipeline gives it one uniform place to do so. 4. **Price the migration.** With urql, plan when the document cache becomes Graphcache and who writes the `keys` and `updates`. With Apollo, plan the type policies up front. 5. **Prototype the hardest screen.** Build the cart with optimistic quantity changes in both, measure the code each needs, and decide on evidence. ## The storefront answer For a small storefront whose catalogue pages dominate and whose cart is the only heavily mutated area, urql with the document cache is a reasonable start, with Graphcache adopted when cart mutations and shared product entities make type-based invalidation too coarse. The answer changes if the team needs cache-backed local state across the app or a normalized cache with field policies from the first release; then Apollo Client's defaults cost less than rebuilding them.

  • The team picks urql and later finds the document cache refetching too much. What does the move to Graphcache involve?
    Swap the `cacheExchange` import for the one from `@urql/exchange-graphcache`, then configure it: `keys` for types without an `id`, `updates.Mutation` updaters for mutations that change lists or links, and `optimistic` functions where instant feedback matters. Components stay unchanged, but every screen that depended on `__typename` invalidation or `additionalTypenames` needs retesting.
  • How would you keep a cart drawer's open state in an urql app, given there is no local state in the cache?
    Keep it in React state, context or a small client store, outside the GraphQL cache. urql's cache represents server data only, and Graphcache rejects writes outside its updaters and resolvers. Mixing the drawer flag with cart data in one component is fine; they simply come from two sources.

saying these in an interview costs you the question

  • urql cannot normalize data, so relational apps must use Apollo.
  • Graphcache lets you write to the cache from any event handler.
  • urql has built-in @client fields for local state.
  • Switching urql's cache means rewriting every component's hooks.
  • The smaller client is always the better choice for any app.