A highly customizable and versatile GraphQL client for React
urql is a GraphQL client built around an exchange-based architecture that prioritizes customization and minimal bundle size. Unlike monolithic alternatives, urql uses a middleware-like system where exchanges process GraphQL operations sequentially—deduplication, caching, fetching, and any custom logic you add. This modular approach means you only ship what you use.
The package provides document-based caching out of the box, which works by hashing query documents and storing results indefinitely. For applications requiring normalized caching with relationships between entities, the separate @urql/exchange-graphcache package provides a more sophisticated cache implementation with optimistic updates and offline support. The default implementation is intentionally simple: queries hit the cache, mutations bypass it and go straight to network.
With over 580,000 weekly downloads, urql has become a popular choice for teams that find Apollo Client too heavy or inflexible. It was designed from the ground up to support React, React Native, Vue, Svelte, and Preact through framework-specific bindings. The exchange system makes it straightforward to add authentication, request retries, persisted queries, or file uploads by swapping or adding exchanges to the client configuration.
The project is actively maintained with TypeScript support including full TSDocs, a browser DevTools extension, and a changesets-based release workflow that provides canary builds for testing unreleased features. Version 5.0.1 represents the mature state of a library that balances simplicity for small projects with extensibility for complex production applications.
import { createClient, cacheExchange, fetchExchange } from 'urql';
import { useQuery, useMutation, Provider } from 'urql';
const client = createClient({
url: 'https://api.example.com/graphql',
exchanges: [cacheExchange, fetchExchange],
fetchOptions: () => ({
headers: { authorization: `Bearer ${localStorage.getItem('token')}` }
})
});
function App() {
return (
<Provider value={client}>
<UserProfile userId="123" />
</Provider>
);
}
function UserProfile({ userId }) {
const [result, reexecuteQuery] = useQuery({
query: `
query GetUser($id: ID!) {
user(id: $id) {
name
email
posts { title }
}
}
`,
variables: { id: userId }
});
const [updateResult, updateUser] = useMutation(`
mutation UpdateUser($id: ID!, $name: String!) {
updateUser(id: $id, name: $name) {
id
name
}
}
`);
if (result.fetching) return <p>Loading...</p>;
if (result.error) return <p>Error: {result.error.message}</p>;
const handleUpdate = async () => {
await updateUser({ id: userId, name: 'Updated Name' });
reexecuteQuery({ requestPolicy: 'network-only' });
};
return (
<div>
<h1>{result.data.user.name}</h1>
<p>{result.data.user.email}</p>
<button onClick={handleUpdate}>Update Name</button>
</div>
);
}Single-page applications with moderate GraphQL usage: Teams building React dashboards or admin panels where Apollo's bundle size and complexity aren't justified. The document cache handles typical read-heavy patterns efficiently without normalized cache overhead.
Mobile applications with offline requirements: React Native apps can use the offline exchange to persist cache to AsyncStorage, enabling read operations when connectivity drops. Combined with optimistic updates from graphcache, this provides a responsive offline-first experience.
Microservice architectures with multiple GraphQL endpoints: The exchange system allows routing different operations to different servers by implementing custom exchanges that inspect operation context. This avoids managing multiple client instances.
Projects requiring custom request lifecycle logic: Applications that need to inject telemetry, implement custom retry strategies with exponential backoff, or handle multi-tenant authentication can slot custom exchanges into the pipeline without forking the client library.
Progressive migration from REST: Teams gradually adopting GraphQL can run urql alongside existing fetch-based code with minimal overhead, then expand usage as more endpoints migrate. The small initial footprint reduces friction compared to committing to a larger client upfront.
npm install urqlpnpm add urqlbun add urql