This chapter follows GraphQL clients compared.
Install urql 5 with its peer graphql package:
pnpm add urql graphqlpnpm add @urql/exchange-auth @urql/exchange-retry @urql/exchange-graphcacheurql 5 builds on @urql/core 6. The old defaultExchanges export is gone—compose cacheExchange and fetchExchange explicitly (plus any optional exchanges).
Create the client
import { cacheExchange, createClient, fetchExchange } from "urql";
export const client = createClient({ url: import.meta.env.VITE_GRAPHQL_URL ?? "http://localhost:3000/graphql", exchanges: [cacheExchange, fetchExchange], fetchOptions: () => { const token = localStorage.getItem("token"); return { headers: token ? { Authorization: `Bearer ${token}` } : {}, }; },});Wrap the app with Provider from urql:
import { Provider } from "urql";import { client } from "./urql-client";
root.render( <Provider value={client}> <App /> </Provider>,);Queries with useQuery
import { gql, useQuery } from "urql";
const BooksQuery = gql` query Books { books { id title } }`;
export function BooksList() { const [{ data, fetching, error }] = useQuery({ query: BooksQuery });
if (fetching) return <p>Loading…</p>; if (error) return <p>{error.message}</p>; return ( <ul> {data?.books.map((b) => ( <li key={b.id}>{b.title}</li> ))} </ul> );}Request policies and pause
Control caching behavior with requestPolicy:
| Policy | Behavior |
|---|---|
cache-first |
Use cache if present, else network |
cache-only |
Never hit network |
network-only |
Skip cache read |
cache-and-network |
Return cache, then refresh |
Set pause: true when variables are incomplete so urql skips the request until ready.
Call reexecuteQuery from the hook result to refetch manually.
Mutations with useMutation
import { gql, useMutation } from "urql";
const CreateBook = gql` mutation CreateBook($input: CreateBookInput!) { createBook(input: $input) { id title } }`;
export function AddBook() { const [{ fetching }, execute] = useMutation(CreateBook); return ( <button disabled={fetching} onClick={() => execute({ input: { title: "New" } })} > Add </button> );}The document cache updates for returned entities when IDs and __typename are present; complex graphs need Graphcache (next chapter).
Auth and retry exchanges
import { authExchange } from "@urql/exchange-auth";import { retryExchange } from "@urql/exchange-retry";
createClient({ exchanges: [ cacheExchange, authExchange(async (utils) => ({ addAuthToOperation(operation) { const token = localStorage.getItem("token"); return token ? utils.appendHeaders(operation, { Authorization: `Bearer ${token}` }) : operation; }, refreshAuth: async () => { /* refresh token */ }, didAuthError: (err) => err.graphQLErrors.some((e) => e.extensions?.code === "UNAUTHENTICATED"), })), retryExchange({ maxNumberAttempts: 3 }), fetchExchange, ], url: "/graphql",});Order matters: auth and retry wrap fetch.
Subscriptions
Add subscriptionExchange with a graphql-ws client:
import { subscriptionExchange } from "urql";import { createClient as createWsClient } from "graphql-ws";
const wsClient = createWsClient({ url: "ws://localhost:3000/graphql" });
subscriptionExchange({ forwardSubscription: (operation) => ({ subscribe: (sink) => ({ unsubscribe: wsClient.subscribe( { query: operation.query, variables: operation.variables }, sink, ), }), }),});Place it before fetchExchange in the exchange array.
Check your understanding
Question 1 of 6
0 of 6 checked