Apollo Client vs urql vs Relay 2026: The Best GraphQL Client for Your React App
Almost every data-driven React app eventually has to talk to a GraphQL API — a headless CMS, a Shopify storefront, an internal gateway, or your own schema. And once you do, you face a decision that shapes your whole data layer: which GraphQL client do you build on? The client is not just a fetcher. It owns caching, cache invalidation after mutations, loading and error states, optimistic updates, subscriptions, and increasingly how you integrate with Suspense and Server Components. Choose wrong and you either carry features you never use or hit a caching wall months in.
In 2026 three clients dominate the React ecosystem: Apollo Client, urql, and Relay. They sit at very different points on the weight, flexibility and opinionation spectrum, and the right pick depends as much on your team and timeline as on the features themselves.
This guide breaks down what each one is, how they compare on the things that actually matter — caching, bundle size, developer experience, code generation, and modern React support — and which to choose for your project, whether you are building your own app or shipping a template others will buy.
GraphQL powers the data layer behind modern React apps
The Three at a Glance
| Client | What it is | Caching model | Bundle size | Best for |
|---|---|---|---|---|
| Apollo Client | Feature-complete, batteries-included GraphQL client | Normalized by default | Largest | Enterprise apps, biggest ecosystem, full features out of the box |
| urql | Lightweight, modular client composed from exchanges | Document cache; normalized via Graphcache | Smallest | Most React/Next.js apps wanting size + flexibility |
| Relay | Meta's compiler-driven, fragment-first client | Normalized store + compiler | Moderate (build-time heavy) | Large, long-lived apps at scale |
Apollo Client: The Feature-Complete Standard
Apollo Client is the oldest and most widely deployed of the three, and it is the default most teams reach for. Its headline feature is a normalized in-memory cache: every entity is stored once by its ID, so when a mutation updates a record, every query that references that record updates automatically. That is the behavior people mean when they say "GraphQL caching just works."
Beyond caching, Apollo is genuinely batteries-included — local state management, request batching, optimistic UI, polling, pagination helpers, subscriptions, persisted queries, and the best devtools of the three. The useQuery and useMutation hooks are declarative and familiar:
npm install @apollo/client graphql"use client";
import { useQuery, gql } from "@apollo/client";
const GET_LISTINGS = gql`
query GetListings {
listings {
id
title
price
}
}
`;
export function Listings() {
const { data, loading, error } = useQuery(GET_LISTINGS);
if (loading) return <p>Loading…</p>;
if (error) return <p>Something went wrong.</p>;
return (
<ul>
{data.listings.map((l) => (
<li key={l.id}>{l.title}</li>
))}
</ul>
);
}The catch is weight and configuration. Apollo ships the most code of the three, and its normalized cache — while powerful — requires you to understand type policies, field policies and cache IDs to get pagination and manual updates right. For a small app that may be more machinery than you need. But Apollo also has an official experimental integration for the Next.js App Router and React Server Components, the most Stack Overflow answers, and the most third-party tutorials. When something breaks at 2am, that ecosystem matters.
urql: The Lightweight, Composable Client
urql (from the team at The Guild) is built on a different philosophy: start small, add only what you need. Its core is a few kilobytes and ships with a simple document cache — results are cached by the query document and variables, and invalidated by type after mutations. For a large share of apps, that is enough and far simpler than reasoning about a normalized store.
What makes urql scale is exchanges: small, composable middleware units you add to the client for caching, authentication, retries, request deduplication and more. You assemble exactly the pipeline you want:
npm install urql graphql"use client";
import { Provider, createClient, cacheExchange, fetchExchange, useQuery } from "urql";
const client = createClient({
url: "/api/graphql",
exchanges: [cacheExchange, fetchExchange],
});
const LISTINGS = `
query { listings { id title price } }
`;
function Listings() {
const [{ data, fetching, error }] = useQuery({ query: LISTINGS });
if (fetching) return <p>Loading…</p>;
if (error) return <p>Something went wrong.</p>;
return <ul>{data.listings.map((l) => <li key={l.id}>{l.title}</li>)}</ul>;
}
export default function App() {
return (
<Provider value={client}>
<Listings />
</Provider>
);
}When you outgrow the document cache, you swap in Graphcache, urql's normalized caching exchange, which gives you Apollo-style entity normalization and optimistic updates — but only when you opt in. That upgrade path is urql's biggest advantage: you do not pay for normalization until you need it.
The honest trade-offs: urql's ecosystem is smaller than Apollo's, some advanced patterns require assembling exchanges yourself, and the official RSC story is less turnkey than Apollo's. But for most new React and Next.js apps, urql hits the best balance of bundle size, flexibility and simplicity.
Relay: The Compiler-Driven Power Tool
Relay is Meta's GraphQL client, built to run Facebook-scale applications, and it is unapologetically opinionated. Its defining idea is fragment colocation: each component declares the exact slice of data it needs as a GraphQL fragment, and Relay's build-time compiler stitches those fragments into optimized queries. The result is a data layer where over-fetching is structurally impossible and every component's data dependencies are explicit.
npm install react-relay relay-runtime
npm install --save-dev relay-compilerimport { graphql, useFragment } from "react-relay";
// Each component owns the fragment for the data it renders.
const listingFragment = graphql`
fragment ListingRow_listing on Listing {
id
title
price
}
`;
export function ListingRow({ listing }) {
const data = useFragment(listingFragment, listing);
return <li>{data.title}</li>;
}That discipline is Relay's superpower and its tax. It delivers the best performance and the most predictable data loading at scale, with first-class support for pagination (connections), deferred and streamed data, and a normalized store that is tightly integrated with the compiler. But it requires a Relay-compliant schema (global object IDs, a node field, connection conventions), a compiler step in your build, and a mental model that takes real time to internalize.
For a startup shipping its first product, that is usually too much. For a large, long-lived app with many engineers and a schema you control, Relay's guarantees are exactly what keep the data layer sane as the codebase grows.
Feature Comparison
| Feature | Apollo Client | urql | Relay |
|---|---|---|---|
| Normalized cache | Yes (default) | Opt-in (Graphcache) | Yes (with compiler) |
| Document cache | No | Yes (default) | No |
| Bundle size | Largest | Smallest | Moderate (build-time heavy) |
| Build step required | No | No | Yes (compiler) |
| Fragment colocation | Supported | Supported | Required / core |
| Local state management | Yes | Via exchanges | Limited |
| Subscriptions | Yes | Yes (exchange) | Yes |
| Optimistic updates | Yes | Yes (Graphcache) | Yes |
| Devtools | Excellent | Good | Good |
| Code generation | GraphQL Code Generator | GraphQL Code Generator | Built-in compiler |
| Next.js App Router / RSC | Official experimental | Lightweight, manual | Works, more setup |
| Learning curve | Moderate | Low | High |
Caching: The Decision That Really Matters
The single biggest difference between these clients is how they cache, because caching is where GraphQL clients earn their keep — and where they bite you.
The practical reading: if automatic, granular cache updates after every mutation are central to your UX, Apollo and Relay give that out of the box, while urql asks you to opt in. If your app is mostly reads with occasional writes, urql's document cache is simpler and lighter. And if you only need request deduplication and background refetching — not GraphQL-aware normalization — you may not need a dedicated client at all; a thin fetcher with TanStack Query can be enough.
The right caching model keeps your data layer fast and consistent
Code Generation and TypeScript
In 2026 you should not be hand-writing GraphQL response types. All three clients integrate with GraphQL Code Generator to produce typed hooks and documents from your schema and operations, giving you end-to-end type safety from query to component. Relay goes further with its own compiler, which generates artifacts and types as a required part of the workflow rather than an optional add-on.
If type safety across your whole stack is the goal — and it should be — pair any of these with codegen and a typed schema. It is the same discipline that separates a template you can refactor from one you are afraid to touch, a theme we cover in what makes code production-ready.
Performance and Bundle Size
All three run in the browser as client-side data layers, so in a Next.js app they belong in client components, with initial data fetched on the server and hydrated into the cache where possible. The weight differences:
As always, measure with your own bundler rather than trusting headline numbers, and only import the caching and middleware you genuinely use.
Which One Should You Choose?
Whichever you pick, the client is a client-component concern, so wrap it accordingly and keep your fetching on the server where you can — the same discipline behind a clean tech stack. And if you are still deciding whether GraphQL is even the right API layer for your product, start with our REST vs GraphQL vs tRPC comparison before you commit to a client at all.
The Bottom Line
Three clients, three bets:
Pick urql for pragmatism, Apollo for completeness, and Relay for scale — and remember the client is only the data layer. What turns it into a product your users (and buyers) trust is how well you assemble it around a typed schema, a sensible cache strategy, and a clean overall tech stack. Many GraphQL apps also sit in front of a headless CMS — a common place the client's caching choices show up first.
Building a GraphQL-powered dashboard, storefront or SaaS on one of these? List your template on CodeCudos — every listing is quality-scored for TypeScript coverage, security and documentation — or browse production-ready templates to start from a proven base instead of a blank schema.
