Best GraphQL Clients for React in 2026
TL;DR
Apollo Client for complex apps with normalized caching; urql for lightweight needs; TanStack Query + fetch for REST-first teams adding GraphQL. Apollo Client (~2M weekly downloads) is the most feature-complete but heaviest option. urql (~600K downloads) is modular and 3-4x smaller. TanStack Query (~5M downloads) handles GraphQL as plain async functions — no schema awareness but maximum flexibility.
Key Takeaways
- Apollo Client: ~2M weekly downloads — full-featured normalized cache, 47KB gzipped
- urql: ~600K downloads — modular, document cache, ~14KB gzipped
- TanStack Query: ~5M downloads — framework-agnostic, works with any fetcher
- Normalized cache vs document cache — Apollo stores by ID; urql stores by query
- GraphQL Code Generator — pairs with any client for type-safe operations
Apollo Client (Full-Featured)
// Apollo Client — normalized cache, optimistic updates
import { ApolloClient, InMemoryCache, gql, useQuery, useMutation } from '@apollo/client';
const client = new ApolloClient({
uri: 'https://api.example.com/graphql',
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
users: {
// Merge paginated results
keyArgs: ['filter'],
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
},
}),
});
// Query with caching
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts { id title }
}
}
`;
function UserProfile({ userId }: { userId: string }) {
const { data, loading, error } = useQuery(GET_USER, {
variables: { id: userId },
// Apollo checks normalized cache — no network request if user already fetched
});
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <div>{data.user.name}</div>;
}
// Apollo — optimistic updates (UI updates before server responds)
const UPDATE_USER = gql`
mutation UpdateUser($id: ID!, $name: String!) {
updateUser(id: $id, name: $name) {
id
name
}
}
`;
function EditUserForm({ user }) {
const [updateUser] = useMutation(UPDATE_USER, {
optimisticResponse: {
updateUser: {
__typename: 'User',
id: user.id,
name: 'New Name', // Show immediately
},
},
});
return (
<button onClick={() => updateUser({ variables: { id: user.id, name: 'New Name' } })}>
Update (shows instantly, syncs in background)
</button>
);
}
Best for: Apps with complex data graphs, cross-component cache sharing, optimistic updates.
urql (Lightweight, Modular)
// urql — composable exchanges (middleware)
import { createClient, cacheExchange, fetchExchange, Provider } from 'urql';
const client = createClient({
url: 'https://api.example.com/graphql',
exchanges: [
cacheExchange, // Document cache (simple, predictable)
fetchExchange, // Network layer
],
});
// Wrap app
function App() {
return (
<Provider value={client}>
<Router />
</Provider>
);
}
// urql hooks — similar to Apollo but lighter
import { useQuery, useMutation } from 'urql';
const GetUserQuery = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
function UserProfile({ userId }: { userId: string }) {
const [result] = useQuery({
query: GetUserQuery,
variables: { id: userId },
// requestPolicy: 'cache-first' | 'network-only' | 'cache-and-network'
requestPolicy: 'cache-first',
});
const { data, fetching, error } = result;
if (fetching) return <Spinner />;
if (error) return <Error message={error.message} />;
return <div>{data?.user.name}</div>;
}
// urql — normalized cache (optional, separate package)
import { createClient } from 'urql';
import { cacheExchange } from '@urql/exchange-graphcache';
const client = createClient({
url: 'https://api.example.com/graphql',
exchanges: [
cacheExchange({
// Optional: define schema for full normalization
keys: {
User: (data) => data.id,
Post: (data) => data.id,
},
updates: {
Mutation: {
createPost(result, args, cache) {
// Manually update cache after mutation
cache.invalidate('Query', 'posts');
},
},
},
}),
fetchExchange,
],
});
Best for: Apps that need GraphQL features without Apollo's overhead. Modular — add normalization when you need it.
TanStack Query + GraphQL
// TanStack Query — GraphQL as plain async functions
// No GraphQL client library needed — just fetch
import { useQuery, useMutation } from '@tanstack/react-query';
const fetchUser = async (userId: string) => {
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
variables: { id: userId },
}),
});
const { data, errors } = await response.json();
if (errors) throw new Error(errors[0].message);
return data.user;
};
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return <div>{data?.name}</div>;
}
// TanStack Query — works great with graphql-request
import { request } from 'graphql-request';
import { useQuery } from '@tanstack/react-query';
const GET_USER = `
query GetUser($id: ID!) {
user(id: $id) { id name email }
}
`;
function UserProfile({ userId }: { userId: string }) {
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => request('/graphql', GET_USER, { id: userId }),
});
return <div>{data?.user.name}</div>;
}
Best for: Teams already using TanStack Query for REST who want GraphQL without a second cache layer.
Bundle Size Comparison
| Client | Bundle Size (gzip) | Normalized Cache | Real-time | SSR Support |
|---|---|---|---|---|
| Apollo Client | ~47KB | ✅ Built-in | ✅ Subscriptions | ✅ |
| urql + graphcache | ~14KB + 12KB | ✅ Optional | ✅ Subscriptions | ✅ |
| TanStack Query + graphql-request | ~5KB + 5KB | ❌ | ❌ (manual) | ✅ |
| React Query + urql core | Mix | ❌ | ✅ | ✅ |
Subscriptions (Real-time)
// Apollo Client — WebSocket subscriptions
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
const wsLink = new GraphQLWsLink(createClient({
url: 'wss://api.example.com/graphql',
}));
const MESSAGES_SUBSCRIPTION = gql`
subscription OnMessage($roomId: ID!) {
message(roomId: $roomId) {
id text author { name }
}
}
`;
function ChatRoom({ roomId }) {
const { data } = useSubscription(MESSAGES_SUBSCRIPTION, {
variables: { roomId },
});
// data.message updates automatically
}
// urql — subscriptions via wonka exchanges
import { subscriptionExchange } from 'urql';
import { createClient } from 'graphql-ws';
const wsClient = createClient({ url: 'wss://api.example.com/graphql' });
const client = createClient({
url: 'https://api.example.com/graphql',
exchanges: [
cacheExchange,
subscriptionExchange({
forwardSubscription(request) {
return { subscribe: (sink) => ({ unsubscribe: wsClient.subscribe(request, sink) }) };
},
}),
fetchExchange,
],
});
When to Choose
| Scenario | Pick |
|---|---|
| Complex app with cross-component data sharing | Apollo Client |
| Need normalized cache without Apollo's size | urql + graphcache |
| Team already uses TanStack Query | TanStack Query + fetch/graphql-request |
| Simple GraphQL queries, no real-time | urql (smallest) |
| Subscriptions are core to the app | Apollo Client or urql |
| Multi-framework app (React + RN + Vue) | urql or TanStack Query |
| Enterprise app with Apollo Studio | Apollo Client |
Compare GraphQL client package health on PkgPulse.
See the live comparison
View apollo client vs. urql on PkgPulse →