URQL GraphQL Client Patterns
Quick Guide: Use URQL for GraphQL APIs when you need a lightweight, customizable client with exchange-based architecture. Start minimal with document caching, add normalized caching via Graphcache when needed. Bundle size is ~12KB gzipped (core), ~20KB with Graphcache. Exchange order is critical: synchronous exchanges before asynchronous, fetchExchange always last. v6+ defaults to GET for small queries - set
preferGetMethod: falseif your server only supports POST. Current version: @urql/core v6.0.1 (urql v5.0.1)
<critical_requirements>
CRITICAL: Before Using This Skill
(You MUST configure exchange order correctly - synchronous exchanges (cacheExchange) before asynchronous (fetchExchange))
(You MUST include __typename in optimistic responses for Graphcache cache normalization)
(You MUST set preferGetMethod: false if your GraphQL server does NOT support GET requests - v6+ defaults to GET for queries under 2048 characters)
</critical_requirements>
Auto-detection: URQL, urql, useQuery, useMutation, useSubscription, cacheExchange, fetchExchange, Graphcache, exchanges, gql, Client
When to use:
- Fetching data from GraphQL APIs
- Applications needing lightweight GraphQL client (~12KB core)
- Projects requiring customizable middleware via exchanges
- Progressive enhancement: start simple, add complexity as needed
- Real-time updates with GraphQL subscriptions
When NOT to use:
- REST APIs (use your data fetching solution instead)
- When team already has deep expertise in another GraphQL client and no bundle concerns
- Simple APIs without caching needs (consider fetch directly)
Key patterns covered:
- Client setup with exchange pipeline
- useQuery for queries with loading, error, and data states
- useMutation with optimistic updates via Graphcache
- useSubscription for real-time WebSocket data
- Exchange architecture and custom exchanges
- Document caching vs normalized caching (Graphcache)
- Request policies and caching strategies
- Authentication with authExchange
Detailed Resources:
- examples/core.md - Client setup, useQuery, useMutation, error handling
- examples/exchanges.md - Exchange architecture, Graphcache, auth, retry
- examples/subscriptions.md - Real-time WebSocket subscriptions
- examples/v6-features.md - v6 breaking changes, GET behavior, migration
- reference.md - Decision frameworks, anti-patterns, API reference
<philosophy>
Philosophy
URQL follows the principle of progressive enhancement. The core package provides document caching and basic fetching, while advanced features like normalized caching, authentication, and offline support are added through exchanges.
Core Principles:
- Minimal by Default: Start with ~12KB core, add features as needed
- Exchange-Based Architecture: Middleware-style plugins for extensibility
- Stream-Based Operations: All operations are Observable streams via Wonka
- Document Caching Default: Simple query+variables hash caching, opt-in normalized cache
URQL's Data Flow:
- Component requests data via useQuery/useMutation
- Operation flows through exchange pipeline (cache -> auth -> retry -> fetch)
- Each exchange can inspect, modify, or short-circuit the operation
- Results flow back through exchanges in reverse
- Multiple results can emit over time (cache update triggers new emission)
Three Architectural Layers:
- Bindings - Framework integrations (React, Vue, Svelte, Solid)
- Client - Core engine managing operations and coordinating exchanges
- Exchanges - Plugins providing functionality (caching, fetching, auth)
<patterns>
Core Patterns
Client Setup
Configure the Client with exchanges in the correct order. Sync exchanges (cacheExchange) before async (fetchExchange).
import { Client, cacheExchange, fetchExchange } from "urql";
const client = new Client({
url: GRAPHQL_ENDPOINT,
exchanges: [cacheExchange, fetchExchange],
});
Wrap your app with <Provider value={client}> to enable hooks. See examples/core.md for full setup.
useQuery
Returns a [result, reexecuteQuery] tuple. Always handle all states: fetching, error, data.
const [result, reexecuteQuery] = useQuery<UsersData, UsersVariables>({
query: USERS_QUERY,
variables: { limit: DEFAULT_PAGE_SIZE },
requestPolicy: "cache-and-network",
});
const { data, fetching, error, stale } = result;
if (fetching && !data) return <Skeleton />; // Initial load only
if (error && !data) return <Error message={error.message} />;
Key: check fetching && !data for initial load vs background refresh. Use pause: !userId for conditional queries. See examples/core.md for full examples.
useMutation
Returns a [result, executeMutation] tuple. The execute function returns a Promise.
const [result, executeMutation] = useMutation<CreatePostData>(CREATE_POST);
const response = await executeMutation({ input });
if (response.error) {
// Handle error
return;
}
Disable form inputs during result.fetching. See examples/core.md for create/update/delete patterns.
Exchange Pipeline
Exchanges are middleware that process operations and results. Order matters critically.
exchanges: [
mapExchange, // 1. Error handling (catches all errors)
cacheExchange, // 2. Sync cache (fast path)
authExchange, // 3. Auth headers
retryExchange, // 4. Retry logic
fetchExchange, // 5. Network (always last)
];
See examples/exchanges.md for auth, retry, Graphcache, and custom exchange patterns.
Graphcache (Normalized Caching)
Upgrade from document cache to normalized cache when you need automatic entity deduplication, optimistic updates, or cache manipulation after mutations.
import { cacheExchange } from "@urql/exchange-graphcache";
cacheExchange({
keys: { Product: (data) => data.sku as string },
updates: {
Mutation: {
createTodo: (result, _args, cache) => {
/* update list */
},
},
},
optimistic: {
toggleTodo: (args) => ({
__typename: "Todo",
id: args.id,
completed: args.completed,
}),
},
});
Always include __typename in optimistic responses. See examples/exchanges.md for full Graphcache configuration.
Request Policies
| Policy | Behavior | Use Case |
|---|---|---|
cache-first | Return cached if available, else fetch (default) | Most queries |
cache-only | Only return cached, never fetch | Offline-first |
network-only | Always fetch, skip cache read | Critical fresh data |
cache-and-network | Return cached immediately, then fetch and update | Stale-while-revalidate |
Use cache-and-network for best UX in most cases. Force refetch with reexecuteQuery({ requestPolicy: "network-only" }).
Subscriptions
Real-time data via WebSocket using subscriptionExchange with graphql-ws.
const [result] = useSubscription<NotificationData>({
query: NOTIFICATION_SUBSCRIPTION,
variables: { userId },
pause: !userId,
});
Subscriptions auto-unsubscribe on unmount. Accumulate events with a reducer and memoized handler. See examples/subscriptions.md for setup and advanced patterns.
Error Handling
URQL wraps all errors in CombinedError, which can contain both networkError and graphQLErrors. GraphQL allows partial data with errors -