Why Choose URQL for React Apps?
Imagine your React project growing, API calls increasing, and Apollo Client starting to lag while the bundle bloats to 300KB. You consider migration but fear losing functionality. URQL solves this: its modular exchange architecture lets you plug in only what you need, shrinking the bundle to 7KB gzip—3x smaller than Apollo. Over the years, we've configured URQL for dozens of projects, from startups to enterprise solutions, consistently achieving a 40% reduction in TTFB and a leaner codebase. With proper cache configuration, server cost savings can reach 30%.
Typical pain points with GraphQL clients: bloated bundles, complex cache setup, and authentication headaches. URQL addresses these with modular exchanges—you only load what's required. For example, authExchange handles tokens, retryExchange retries on network errors, and subscriptionExchange manages WebSocket subscriptions. This gives you flexibility and control. Contact us for a consultation on configuring URQL for your project.
| Criteria | URQL | Apollo Client |
|---|---|---|
| Bundle size | ~7KB gzip | ~25KB gzip |
| Architecture | Exchanges (chain) | Monolithic |
| Normalized cache | Graphcache (optional) | Built-in |
| SSR support | Native (next-urql) | getDataFromTree |
| Subscriptions | subscriptionExchange | Built-in |
Real-World Implementation
In an e-commerce project with a product catalog of 50,000 items, we replaced Apollo Client with URQL. Results:
- Bundle size shrank from 250KB to 80KB gzip.
- TTFB improved by 45%, LCP by 20%.
- Server costs dropped by 30% due to fewer redundant requests.
- Developers no longer spent time on manual cache invalidation—Graphcache handled it automatically.
This case confirms that URQL is not only lighter but also more performant in complex projects. If you want to replicate this success, get a consultation on configuring URQL for your project.
How to Configure Exchanges for Authentication and Retries
Client and exchange configuration is key. Below is a typical production setup:
npm install urql graphql @urql/exchange-auth @urql/exchange-retry graphql-ws @urql/exchange-graphcache
// lib/urql/client.ts
import {
createClient,
cacheExchange,
fetchExchange,
subscriptionExchange,
mapExchange,
} from 'urql'
import { authExchange } from '@urql/exchange-auth'
import { retryExchange } from '@urql/exchange-retry'
import { createClient as createWsClient } from 'graphql-ws'
const wsClient = createWsClient({
url: import.meta.env.VITE_WS_URL ?? 'ws://localhost:4000/graphql',
connectionParams: () => ({
authorization: `Bearer ${localStorage.getItem('token')}`,
}),
})
export const urqlClient = createClient({
url: import.meta.env.VITE_GRAPHQL_URL ?? '/graphql',
exchanges: [
mapExchange({
onError(error) {
if (error.response?.status === 401) {
authStore.logout()
}
console.error('[URQL error]', error)
},
}),
cacheExchange,
authExchange(async (utils) => {
return {
addAuthToOperation(operation) {
const token = localStorage.getItem('token')
if (!token) return operation
return utils.appendHeaders(operation, {
Authorization: `Bearer ${token}`,
})
},
didAuthError(error) {
return error.graphQLErrors.some(
(e) => e.extensions?.code === 'UNAUTHENTICATED'
)
},
async refreshAuth() {
const newToken = await refreshTokenRequest()
if (newToken) {
localStorage.setItem('token', newToken)
} else {
localStorage.removeItem('token')
authStore.logout()
}
},
willAuthError() {
const token = localStorage.getItem('token')
return !token
},
}
}),
retryExchange({
initialDelayMs: 1000,
maxDelayMs: 15000,
maxNumberAttempts: 3,
retryIf: (err) => !!(err && err.networkError),
}),
fetchExchange,
subscriptionExchange({
forwardSubscription(request) {
const input = { ...request, query: request.query ?? '' }
return {
subscribe(sink) {
const dispose = wsClient.subscribe(input, sink)
return { unsubscribe: dispose }
},
}
},
}),
],
})
When to Use the Graphcache Normalized Cache?
For simple cases, cacheExchange suffices. But when you have lots of interrelated data, Graphcache helps. It automatically updates the cache on mutations by ID. We often use it in product catalogs and social networks. Development savings: no need to write custom invalidation handlers—the codebase shrinks by 30%.
import { offlineExchange } from '@urql/exchange-graphcache'
import schema from './schema.json'
const cache = offlineExchange({
schema,
keys: {
Product: (data) => data.id ?? null,
User: (data) => data.id ?? null,
Category: (data) => data.id ?? null,
},
resolvers: {
Query: {
product: (_, args) => ({ __typename: 'Product', id: args.id }),
},
},
updates: {
Mutation: {
createProduct: (result, _args, cache) => {
cache.invalidate('Query', 'products')
},
deleteProduct: (result, args, cache) => {
cache.invalidate({ __typename: 'Product', id: args.id as string })
},
updateProduct: (_result, _args, cache) => {
// normalized cache updates automatically by id
},
},
},
optimistic: {
updateProduct: (args) => ({
__typename: 'Product',
id: args.id,
...(args.input as object),
}),
},
})
Code Generation for Types
URQL uses the same @graphql-codegen/client-preset. Example configuration:
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: 'http://localhost:4000/graphql',
documents: 'src/**/*.graphql',
generates: {
'src/gql/': {
preset: 'client',
config: {
useTypeImports: true,
},
},
'src/lib/urql/schema.json': {
plugins: ['introspection'],
},
},
}
export default config
Using URQL in React
Hooks for Queries, Mutations, and Subscriptions
// features/products/useProducts.ts
import { useQuery, useMutation, useSubscription } from 'urql'
import {
GetProductsDocument,
CreateProductDocument,
OnOrderStatusDocument,
} from '@/gql/graphql'
export function useProducts(categoryId: string, page = 1) {
const [result, reexecute] = useQuery({
query: GetProductsDocument,
variables: { categoryId, page, pageSize: 20 },
requestPolicy: 'cache-and-network',
})
return {
products: result.data?.products.items ?? [],
total: result.data?.products.total ?? 0,
hasNextPage: result.data?.products.hasNextPage ?? false,
fetching: result.fetching,
error: result.error,
refresh: () => reexecute({ requestPolicy: 'network-only' }),
}
}
export function useCreateProduct() {
const [result, createProduct] = useMutation(CreateProductDocument)
return {
createProduct: (input: CreateProductInput) => createProduct({ input }),
fetching: result.fetching,
error: result.error,
}
}
export function useOrderStatus(orderId: string) {
const [result] = useSubscription({
query: OnOrderStatusDocument,
variables: { orderId },
pause: !orderId,
})
return {
status: result.data?.orderStatusChanged?.status,
fetching: result.fetching,
error: result.error,
}
}
| Caching Policy | Description | When to Use |
|---|---|---|
| cache-first | Cache first, then network | Rarely changing data |
| cache-and-network | Return cache, but update | Frequently requested, up-to-date |
| network-only | Always network | Critical data (e.g., balance) |
| cache-only | Only cache | Offline mode |
Process and Timelines
- Analyze project requirements and architecture.
- Design the exchange chain and caching scheme.
- Implement client configuration, code generation, and hooks.
- Test with real scenarios (auth, subscriptions, errors).
- Deploy and monitor performance.
Setup takes from 2 to 4 days depending on complexity. Pricing is determined after assessment.
Common URQL Configuration Mistakes
- Forgetting to add authExchange to the chain—requests go without tokens.
- Using cacheExchange instead of graphcache for complex relations—leading to manual invalidation.
- Not configuring retryExchange—data loss on transient network errors.
- Skipping willAuthError—causing unnecessary token refresh requests.
What’s Included
- Optimal exchange chain configuration
- Authentication setup (token, refresh)
- Normalized cache integration when needed
- TypeScript type code generation
- React hooks for queries/mutations/subscriptions
- SSR support (Next.js)
- API and usage documentation
- Team training (1 hour)
- 30-day post-deployment support
You can verify URQL’s effectiveness by ordering a demo setup on your project. If you need professional URQL configuration, contact us—we’ll tailor the setup to your project and guarantee results.







