Skip to main content

Best GraphQL Clients for React in 2026

·PkgPulse Team
0

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 — 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

ClientBundle Size (gzip)Normalized CacheReal-timeSSR Support
Apollo Client~47KB✅ Built-in✅ Subscriptions
urql + graphcache~14KB + 12KB✅ Optional✅ Subscriptions
TanStack Query + graphql-request~5KB + 5KB❌ (manual)
React Query + urql coreMix

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

ScenarioPick
Complex app with cross-component data sharingApollo Client
Need normalized cache without Apollo's sizeurql + graphcache
Team already uses TanStack QueryTanStack Query + fetch/graphql-request
Simple GraphQL queries, no real-timeurql (smallest)
Subscriptions are core to the appApollo Client or urql
Multi-framework app (React + RN + Vue)urql or TanStack Query
Enterprise app with Apollo StudioApollo Client

Compare GraphQL client package health on PkgPulse.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.