skip to content

urql

urql keeps its core small and pushes features into exchanges, letting you choose between simple document caching and a full normalized graph cache. It comes up as the middle ground between Apollo's weight and writing a client yourself.

on this pageshow

questions

6

In urql 5 for React, how do you create and provide a Client, and what does exchanges: [cacheExchange, fetchExchange] do?

level: juniorimportance: must knowfreq 38%

answer

  1. one client, one pipeline
  2. Provider puts it in context
  3. required array since core 4.0
  4. cache answers, fetch sends
  5. results travel back in reverse

basics

~20 s

An urql Client is built with createClient({ url, exchanges }) and handed to components through Provider. The required exchanges array is a pipeline: cacheExchange answers from the document cache, fetchExchange sends everything else over HTTP.

solid answer

~50 s

You create one `Client` with `createClient({ url, exchanges: [cacheExchange, fetchExchange] })` and wrap the app in `<Provider value={client}>`; hooks such as `useQuery` read it from context and throw in development when no Provider is found. The `exchanges` array is the client's whole behaviour: operations flow through it left to right and results come back right to left. `cacheExchange` is the default document cache, which returns a stored result when the request policy allows and forwards the operation otherwise; `fetchExchange` sends queries and mutations over HTTP and forwards only what it does not handle. The option has been required since `@urql/core` 4.0, with no `defaultExchanges` fallback, and the `Client` itself deduplicates identical in-flight operations, which is why `dedupExchange` was removed in core 5.0. In urql 5, queries go out as GET while the URL stays under 2048 characters.

code

tsx · 27 lines
tsx
import { Client, Provider, cacheExchange, fetchExchange, gql, useQuery } from 'urql';

const client = new Client({
  url: '/graphql',
  exchanges: [cacheExchange, fetchExchange],
});

const ProductsQuery = gql`
  query Products {
    products { id name }
  }
`;

function ProductList() {
  const [{ data, fetching, error }] = useQuery({ query: ProductsQuery });
  if (fetching) return <p>Loading…</p>;
  if (error) return <p>{error.message}</p>;
  return <ul>{data?.products.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}

export function App() {
  return (
    <Provider value={client}>
      <ProductList />
    </Provider>
  );
}

go deeper

for a junior

Recall the three pieces: a Client with a url and an exchanges array, a Provider holding it, and hooks that read it. Name what cacheExchange and fetchExchange each do.

for a middle

Explain the pipeline: operations go left to right, results come back right to left, and the cache stores results on the way back. Say why fetchExchange must come after the cache.

for a senior

Show you know the recent breaking changes: exchanges required since core 4.0, dedupExchange removed in 5.0, GET queries by default in core 6.0, and what each breaks in an upgrade.

for a principal

Frame the exchanges array as the extension point: caching, auth and retries are swapped by editing one array, which keeps components stable while the data layer evolves.

## The three pieces of an urql app urql splits a GraphQL client into three parts, and a React app needs all three. - **The `Client`**, created with `createClient({ ... })` or `new Client({ ... })`. It holds the API `url`, default options such as `requestPolicy`, and the **exchanges** that decide what happens to each operation. - **The `Provider`**, a React context provider exported by `urql`. You render `<Provider value={client}>` near the root so every component below it can reach the same client. - **The hooks**: `useQuery`, `useMutation` and `useSubscription`. Each reads the client from context. In development, a hook rendered outside any `Provider` throws an error that tells you to add one. `useQuery` returns a tuple, much like React's `useState`: the first element is the result state (`data`, `error`, `fetching`, `stale`), and the second is `reexecuteQuery`, a function that runs the query again, optionally with a different request policy. `useMutation` returns the mutation state and an `executeMutation(variables, context?)` function that resolves to the result. ## What the exchanges array is An **exchange** is a small function that receives a stream of operations (queries, mutations, subscriptions and `teardown` signals) and returns a stream of results. The `Client` composes the array into one pipeline: 1. A hook asks the client for a query, and the client turns the request into an **operation** with a key derived from the document and its variables. 2. The operation enters the first exchange. If `cacheExchange` holds a result for that key and the request policy allows it, the result is returned right there and the operation goes no further. 3. Otherwise the operation is forwarded to the next exchange, `fetchExchange`, which sends it to the API. 4. The result travels back through the exchanges **in reverse order**. On the way back, `cacheExchange` stores it under the operation's key. That is why the order in the array is meaningful and not a list of plugins. ## What each default exchange does | Exchange | Handles | Does on the way out | Does on the way back | |---|---|---|---| | `cacheExchange` (from `urql`/`@urql/core`) | queries and mutations | answers cached queries, forwards the rest | stores query results; invalidates by `__typename` after mutations | | `fetchExchange` | queries and mutations (subscriptions only with `fetchSubscriptions`) | sends the HTTP request | turns the response into a result | Both exchanges are imported from `urql`, which re-exports all of `@urql/core`. ## Mistakes that show up during setup - **No `fetchExchange`.** Nothing sends the request. In development the client logs a warning that no exchange handled operations of kind `"query"`, and the component never receives a result. - **`fetchExchange` before `cacheExchange`.** `fetchExchange` does not forward the queries and mutations it sends, so the cache never sees them, and nothing is ever cached. - **Importing `dedupExchange`.** It was deprecated in core 4.0 and removed in 5.0; the `Client` now deduplicates identical in-flight operations itself, so two product cards asking for the same query at once share one request. - **Creating the client inside a component.** Each render then builds a fresh `Client` with an empty cache. Create it once, at module level or in a memoized initializer. - **Omitting `exchanges`.** It is required. There is no longer a `defaultExchanges` export to fall back on. ## What changed in recent majors | Release | Change you will meet | |---|---| | `@urql/core` 3.1 | `mapExchange` replaced `errorExchange`, which remains only as a deprecated alias | | `@urql/core` 4.0 | `exchanges` became required; `defaultExchanges` removed; deduplication moved into the `Client` | | `@urql/core` 5.0 | `dedupExchange` removed | | `@urql/core` 6.0 / `urql` 5.0 | queries are sent as GET when the URL is under 2048 characters | The last change matters when an API only accepts POST: after upgrading, its queries start failing until the server accepts GET or the client sets `preferGetMethod: false`. Mutations are still sent as POST. ## A minimal storefront setup A small storefront starts exactly like this: one client with the two default exchanges, a `Provider` around the app, and a product list that calls `useQuery`. Later changes, such as adding a normalized cache or an authentication exchange, are edits to the `exchanges` array rather than to the components, which is the design idea urql is built on.

  • What does urql 5 change about the HTTP method used for queries, and who notices?
    Since `@urql/core` 6.0, which `urql` 5.0 depends on, `fetchExchange` sends queries as GET when the query string plus variables keeps the URL under 2048 characters, and falls back to POST above that. Mutations stay POST. A server that only accepts POST starts rejecting queries after the upgrade; either enable GET on the server or pass `preferGetMethod: false` to the client.
  • Two product cards mount at the same time and both run the same query with the same variables. How many requests does urql send?
    One. The `Client` gives both operations the same key, from the document and its variables, and while one is in flight it does not dispatch a duplicate; both hooks subscribe to the same result stream. This used to be `dedupExchange`'s job, which is why that exchange was deprecated in core 4.0 and removed in 5.0.

The exchanges array works like a row of service counters. Each request walks down the row until a counter can serve it, and the answer is carried back past every earlier counter. Put the counter that sends requests out before the one that keeps copies, and the copy counter never sees anything to keep.

saying these in an interview costs you the question

  • Leaving out exchanges is fine because urql falls back to default exchanges.
  • You still add dedupExchange before cacheExchange to avoid duplicate requests.
  • Exchange order is irrelevant because every exchange sees every operation.
  • fetchExchange caches responses itself, so cacheExchange is optional.
  • useQuery returns an object with loading and refetch fields.
open as a page

In urql, what do the four requestPolicy values do, and when would a storefront screen pick cache-and-network over the default?

level: middleimportance: must knowfreq 34%

basics

~10 s

urql's requestPolicy is cache-first by default, returning a cached result or fetching once. cache-and-network returns the cached result marked stale and refetches, network-only always fetches, and cache-only never sends a request.

open as a page

In urql's default document cache, why does an empty cart keep showing no items after an addToCart mutation succeeds, and how do you fix it?

level: middleimportance: should knowfreq 28%

basics

~20 s

urql's document cache invalidates cached queries whose results contained a __typename that the mutation result contains. An empty cart's lines list carries no CartLine typename, so nothing links it to addToCart; additionalTypenames adds that link.

open as a page

When an urql storefront moves from the document cache to Graphcache, what do its keys, updates and optimistic options configure, and what breaks without them?

level: seniorimportance: should knowfreq 22%

basics

~10 s

Graphcache's keys say how to identify entities without an id, updates.Mutation holds updaters for list and link changes a mutation result cannot express, and optimistic returns a temporary result applied until the server answers.

open as a page

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%

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.

open as a page

In urql, how would you write a mapExchange that adds a cart-session header and signs users out on authentication errors, and where does it go in exchanges?

level: seniorimportance: nice to knowfreq 14%

basics

~20 s

mapExchange takes onOperation, onResult and onError. onOperation returns a copy of the operation with extra fetchOptions headers, onError inspects the CombinedError, and the exchange sits between cacheExchange and fetchExchange so it sees every request and its result.

open as a page