Apollo Client vs urql in 2026: GraphQL Client Libraries
TL;DR
Apollo Client for complex GraphQL with caching needs; urql for lighter-weight GraphQL in React. Apollo Client (~6M weekly downloads) is the full-featured option — normalized cache, devtools, subscriptions, local state. urql (~2M downloads) is modular and smaller, with a document cache by default instead of normalized cache. For simple GraphQL data fetching, urql is often sufficient and easier to configure.
Key Takeaways
- Apollo Client: ~6M weekly downloads — urql: ~2M (npm, March 2026)
- Apollo has normalized cache — entities cached by ID, smarter updates
- urql uses document cache by default — simpler, faster, less overhead
- Apollo has better devtools — Apollo DevTools browser extension is excellent
- Both support subscriptions — via WebSocket or SSE
GraphQL vs REST: When to Use Which
Before choosing a GraphQL client, the first question is whether GraphQL is the right choice at all. GraphQL adds complexity (schema definition, resolver implementation, query language for clients) that isn't always justified.
Use GraphQL when:
- Multiple clients (web, mobile, third-party) need different data shapes from the same API
- Frontend teams need to iterate on data requirements without backend deploys
- You have a complex domain with many related entities and want to avoid N+1 over-fetching
Use REST when:
- Simple CRUD API with well-defined endpoints
- Small team where both frontend and backend are maintained together
- You want the simplest possible API surface
If your team decides on GraphQL, the choice between Apollo Client and urql is your next decision.
Cache Philosophy
Apollo Client — normalized cache (InMemoryCache):
- Each object is stored by type + ID
- Same object in multiple queries → one cache entry
- Updates in one query automatically reflect in others
- Powerful but complex to configure
- Higher memory usage
urql — document cache (default):
- Caches by query + variables
- Simple: same query → same result
- No normalization → simpler reasoning
- Can opt into normalized cache (urql/graphcache)
- Lower memory usage
Setup
// Apollo Client — setup
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
const client = new ApolloClient({
uri: 'https://api.example.com/graphql',
cache: new InMemoryCache({
typePolicies: {
User: {
fields: {
// Merge strategies for pagination, etc.
},
},
},
}),
});
function App() {
return (
<ApolloProvider client={client}>
<MyApp />
</ApolloProvider>
);
}
// urql — minimal setup
import { createClient, Provider } from 'urql';
const client = createClient({
url: 'https://api.example.com/graphql',
fetchOptions: {
headers: { Authorization: `Bearer ${getToken()}` },
},
});
function App() {
return (
<Provider value={client}>
<MyApp />
</Provider>
);
}
Querying
// Apollo Client — useQuery
import { useQuery, gql } from '@apollo/client';
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts { id title }
}
}
`;
function UserProfile({ id }) {
const { data, loading, error } = useQuery(GET_USER, {
variables: { id },
fetchPolicy: 'cache-first', // 'cache-and-network', 'network-only', etc.
});
if (loading) return <Spinner />;
if (error) return <Error />;
return <div>{data.user.name}</div>;
}
// urql — useQuery
import { useQuery, gql } from 'urql';
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id name email
posts { id title }
}
}
`;
function UserProfile({ id }) {
const [result] = useQuery({ query: GET_USER, variables: { id } });
const { data, fetching, error } = result;
if (fetching) return <Spinner />;
if (error) return <Error />;
return <div>{data.user.name}</div>;
}
Mutations
// Apollo Client — useMutation with cache update
import { useMutation, gql } from '@apollo/client';
const CREATE_POST = gql`
mutation CreatePost($title: String!, $content: String!) {
createPost(title: $title, content: $content) { id title }
}
`;
function NewPostForm() {
const [createPost, { loading, error }] = useMutation(CREATE_POST, {
update(cache, { data: { createPost } }) {
cache.modify({
fields: {
posts(existingPosts = []) {
const newPostRef = cache.writeFragment({
data: createPost,
fragment: gql`fragment NewPost on Post { id title }`,
});
return [...existingPosts, newPostRef];
},
},
});
},
});
const handleSubmit = () =>
createPost({ variables: { title, content } });
}
// urql — useMutation
import { useMutation, gql } from 'urql';
const CREATE_POST = gql`
mutation CreatePost($title: String!, $content: String!) {
createPost(title: $title, content: $content) { id title }
}
`;
function NewPostForm() {
const [result, createPost] = useMutation(CREATE_POST);
const { fetching, error } = result;
const handleSubmit = () =>
createPost({ title, content });
// urql's document cache auto-invalidates related queries
// No manual cache update needed for simple cases
}
The cache update difference is significant in practice. Apollo's normalized cache is powerful but requires you to write explicit cache update code after mutations — you must tell Apollo which cache entries to update or invalidate. urql's document cache automatically invalidates queries that contain the same types as the mutation result. For most CRUD operations, urql's auto-invalidation is sufficient.
Subscriptions
Both support real-time updates via WebSocket:
// Apollo Client — subscriptions
import { useSubscription, gql } from '@apollo/client';
const MESSAGE_ADDED = gql`
subscription MessageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id content author { name }
}
}
`;
function ChatChannel({ channelId }) {
const { data } = useSubscription(MESSAGE_ADDED, {
variables: { channelId },
onData: ({ client, data }) => {
// Update Apollo cache with new message
client.cache.modify({ /* ... */ });
},
});
}
// urql — subscriptions with wonka
import { useSubscription, gql } from 'urql';
const MESSAGE_ADDED = gql`
subscription MessageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id content author { name }
}
}
`;
function ChatChannel({ channelId }) {
const [result] = useSubscription({
query: MESSAGE_ADDED,
variables: { channelId },
});
// result.data contains the latest subscription event
}
Error Handling
// Apollo Client — structured errors
const { data, error } = useQuery(GET_USER, { variables: { id } });
if (error) {
// error.graphQLErrors — errors from the GraphQL layer
// error.networkError — HTTP/network failures
error.graphQLErrors.forEach(e => {
if (e.extensions?.code === 'UNAUTHENTICATED') {
redirectToLogin();
}
});
}
// urql — CombinedError
const [{ data, error }] = useQuery({ query: GET_USER, variables: { id } });
if (error) {
// error.graphQLErrors — GraphQL errors
// error.networkError — network failures
// error.message — combined message
}
TanStack Query as an Alternative
If your team is already using TanStack Query for REST data fetching, consider using it for GraphQL too rather than adding a dedicated GraphQL client:
// TanStack Query + graphql-request (no dedicated client)
import { useQuery } from '@tanstack/react-query';
import { request, gql } from 'graphql-request';
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) { id name email }
}
`;
function UserProfile({ id }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', id],
queryFn: () => request('https://api.example.com/graphql', GET_USER, { id }),
});
}
This approach gives you TanStack Query's excellent caching and background refetching semantics with minimal GraphQL-specific library overhead. You lose normalized caching and subscription support, but gain consistency with the rest of your data fetching layer.
When to Choose
Choose Apollo Client when:
- Large GraphQL schemas with complex entity relationships
- Normalized cache is needed (same entity in many queries)
- Apollo DevTools are valuable for your team
- Optimistic UI updates and rollback
- Local state management via Apollo reactive variables
Choose urql when:
- Simpler GraphQL usage (mostly queries)
- Bundle size matters (~15KB vs Apollo's ~40KB gzipped)
- You want easier configuration without typePolicies
- Document-cache behavior is sufficient (most CRUD apps)
- Team is building with urql's exchange system for customization
Migration Guide
From Apollo Client to urql
The primary API surfaces map directly between the two libraries:
// Apollo Client (old)
import { ApolloClient, InMemoryCache, ApolloProvider, useQuery, gql } from "@apollo/client"
const client = new ApolloClient({
uri: "https://api.example.com/graphql",
cache: new InMemoryCache(),
headers: { Authorization: `Bearer ${token}` },
})
// Wrap app: <ApolloProvider client={client}><App /></ApolloProvider>
const { data, loading, error } = useQuery(GET_USER, { variables: { id } })
// urql (new)
import { Client, Provider, cacheExchange, fetchExchange, useQuery, gql } from "urql"
const client = new Client({
url: "https://api.example.com/graphql",
exchanges: [cacheExchange, fetchExchange],
fetchOptions: () => ({ headers: { Authorization: `Bearer ${token}` } }),
})
// Wrap app: <Provider value={client}><App /></Provider>
const [{ data, fetching, error }] = useQuery({ query: GET_USER, variables: { id } })
The two behavioral differences to plan for: Apollo uses loading as the loading state key while urql uses fetching, and Apollo's normalized cache requires explicit cache update code after mutations while urql's document cache auto-invalidates by type name.
Community Adoption in 2026
Apollo Client reaches approximately 4 million weekly downloads, reflecting its decade-long status as the primary GraphQL client for React. Despite its bundle size (~40KB gzipped) and configuration complexity, it remains the first choice for applications with complex GraphQL schemas where normalized entity caching provides real value — particularly when the same entity (a User, Product, or Order object) appears in many different queries and must stay synchronized.
urql sits at approximately 1 million weekly downloads, growing steadily as its simpler default behavior gains recognition. The exchange system — a composable middleware pipeline for the GraphQL client — has attracted developers who want fine-grained control without Apollo's opinionated cache architecture. urql's bundle size advantage (~15KB vs ~40KB) is meaningful for public-facing applications where JavaScript payload size affects Core Web Vitals.
The broader trend is that many teams are evaluating whether they need a dedicated GraphQL client at all, given TanStack Query's ability to handle GraphQL queries as generic async functions with excellent caching semantics. For teams already using TanStack Query for REST, the overhead of adding Apollo or urql must be justified by specific features (normalized cache, subscriptions, Apollo DevTools).
Fragment Colocation and Code Generation Workflow
GraphQL's ability to colocate data requirements with components is one of its primary advantages over REST, and how each client implements fragment colocation affects the overall developer experience.
Apollo Client's fragment masking (available since Apollo Client 3.7) is inspired by Relay's data masking principle: a component that uses a fragment can only access the fields declared in that fragment, not the full query result. This prevents the "overfetching at the component level" problem where components access fields from other components' fragments, creating implicit coupling. The useFragment hook provides the masked fragment data; TypeScript types are narrowed to only the fragment's fields. This is a significant architecture improvement for large codebases but requires opting in to a more structured data fetching pattern.
urql's document caching is simpler than Apollo's normalized cache but sufficient for many use cases. Every query result is cached by a key derived from the query document and variables. A cache miss triggers a network request; the response is stored by that key. Cache invalidation happens when mutations return updated data for the same document key, or when requestPolicy: 'network-only' bypasses the cache. For applications with simple data requirements (no complex data relationships, no real-time updates), document caching avoids the complexity of normalized cache configuration.
Code generation via @graphql-codegen/cli works with both clients, producing TypeScript types for queries, mutations, and fragments. The output for Apollo Client uses Apollo's TypedDocumentNode type for full TypeScript integration with useQuery and useMutation. The urql plugin produces compatible types. Running graphql-codegen --watch during development keeps types synchronized with schema changes, catching breaking API changes immediately. For teams using schema-first development (the GraphQL schema is the source of truth), code generation is essential for maintaining type safety across schema evolution.
For teams choosing between the two, the practical difference in most applications is cache sophistication: Apollo's normalized cache handles complex update scenarios (optimistic updates, paginated list management, partial updates via cache.modify) that urql's document cache cannot. urql's simplicity wins for straightforward CRUD applications; Apollo wins when the UI requires complex cache coordination that would otherwise require custom state management.
The GraphQL client landscape in 2026 reflects a broader maturation in how teams approach data fetching. Apollo Client's decade of development shows in its normalized cache sophistication and fragment masking support — features that emerged from real production pain at scale. urql's exchange architecture reflects a different lesson: composability is more valuable than a monolithic feature set, because teams inevitably need to customize caching, error handling, and authentication in ways no single design anticipates. The TanStack Query alternative reveals a third perspective — for many teams, a GraphQL client's special features (normalized cache, subscriptions, devtools) are not needed, and a generic async-state manager with graphql-request achieves better results with less cognitive overhead. Choosing between them requires an honest assessment of your schema complexity, caching requirements, and whether your team will benefit from Apollo's deep tooling or urql's simpler defaults.
Compare Apollo Client and urql package health on PkgPulse. Also see tRPC v11 for a type-safe REST alternative and how to set up a modern React project for the full data fetching stack.
Related: Best GraphQL Clients for React in 2026.