skip to content

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.