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
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.
See the live comparison
View apollo client vs. urql on PkgPulse →