← Back to blog
··13 min read

Apollo Client vs urql vs Relay 2026: The Best GraphQL Client for Your React App

ReactNext.jsGraphQLApolloData FetchingTypeScriptSaaS
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

GraphQL powers the data layer behind modern React apps

The Three at a Glance

ClientWhat it isCaching modelBundle sizeBest for
Apollo ClientFeature-complete, batteries-included GraphQL clientNormalized by defaultLargestEnterprise apps, biggest ecosystem, full features out of the box
urqlLightweight, modular client composed from exchangesDocument cache; normalized via GraphcacheSmallestMost React/Next.js apps wanting size + flexibility
RelayMeta's compiler-driven, fragment-first clientNormalized store + compilerModerate (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:

bash
npm install @apollo/client graphql
tsx
"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:

bash
npm install urql graphql
tsx
"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.

bash
npm install react-relay relay-runtime
npm install --save-dev relay-compiler
tsx
import { 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

FeatureApollo ClienturqlRelay
Normalized cacheYes (default)Opt-in (Graphcache)Yes (with compiler)
Document cacheNoYes (default)No
Bundle sizeLargestSmallestModerate (build-time heavy)
Build step requiredNoNoYes (compiler)
Fragment colocationSupportedSupportedRequired / core
Local state managementYesVia exchangesLimited
SubscriptionsYesYes (exchange)Yes
Optimistic updatesYesYes (Graphcache)Yes
DevtoolsExcellentGoodGood
Code generationGraphQL Code GeneratorGraphQL Code GeneratorBuilt-in compiler
Next.js App Router / RSCOfficial experimentalLightweight, manualWorks, more setup
Learning curveModerateLowHigh

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.

  • Apollo normalizes by default. After a mutation, any query referencing the changed entity updates automatically — but you must configure type and field policies for pagination and custom cache updates.
  • urql starts with a document cache that is trivial to reason about: it caches whole results and invalidates them by type on mutation. When you need per-entity updates, you add Graphcache and get normalized behavior deliberately.
  • Relay maintains a normalized store driven by its compiler, so updates are precise and predictable — at the cost of the schema conventions and build step that make it possible.
  • 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

    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:

  • urql is the lightest by default — a compact core plus only the exchanges you add. A simple setup stays tiny.
  • Apollo is the heaviest because it bundles normalized caching and many features by default; you pay for them whether or not you use them.
  • Relay keeps a moderate runtime but pushes significant work to build time via its compiler, so what ships to the browser is lean and highly optimized — the trade is a slower, more involved build.
  • 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?

  • Choose Apollo Client if you want the most complete feature set with the least assembly — normalized caching, local state, subscriptions, devtools and the biggest ecosystem — and you can accept a larger bundle and some cache configuration. It is the safe enterprise default and the best-documented path for the Next.js App Router today.
  • Choose urql if you want a lightweight, flexible client that starts simple and grows with you: a tiny core, a document cache that is easy to reason about, and a clean upgrade path to normalized caching via Graphcache. For most new React and Next.js apps, it is the pragmatic default.
  • Choose Relay if you are building a large, long-lived application with a schema you control and a team that will commit to its compiler and fragment model. It delivers the most disciplined, highest-performance data layer at scale — but it is overkill for small or early-stage projects.
  • 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:

  • Apollo Client — the feature-complete standard: a normalized cache, local state, subscriptions and the largest ecosystem, in exchange for the biggest bundle and the most configuration. The safe pick when you want everything available and well-documented.
  • urql — the lightweight, composable client: a tiny core, exchanges you assemble yourself, a simple document cache, and an opt-in path to normalized caching. The best balance of size and capability for most React and Next.js apps.
  • Relay — the compiler-driven power tool: fragment colocation and a build-time compiler that make over-fetching impossible and performance predictable at scale, at the cost of the steepest learning curve and a Relay-compliant schema. The right choice for large, long-lived apps that commit to its workflow.
  • 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.

    Frequently asked questions

    What is the difference between Apollo Client, urql, and Relay?▾

    Apollo Client is a comprehensive, batteries-included GraphQL client with a normalized in-memory cache, local-state features, request batching, optional persisted queries and mature devtools; it is the most feature-rich and has the largest ecosystem, but ships the most code. urql is a smaller, modular client built around composable exchanges — small middleware units for caching, authentication, retries and more — so you start lean with a simple document cache and opt into normalized caching (Graphcache) only when you need it. Relay is Meta's opinionated client built for scale: it uses a build-time compiler and requires you to colocate GraphQL fragments with components, which produces highly optimized queries and a predictable data layer, but demands a Relay-compliant schema and a steeper learning curve. In short, Apollo is the full-featured standard, urql is the lightweight and composable middle ground, and Relay is the disciplined power tool.

    Which GraphQL client has the smallest bundle size?▾

    urql is the smallest of the three in its base configuration — its core is only a few kilobytes and you add weight only for the exchanges you actually use, so a simple query-and-mutation setup stays very light. Apollo Client is the heaviest because it bundles a normalized cache, local-state handling and many features by default. Relay sits in between at runtime, but it is unusual in that much of its work happens at build time through its compiler rather than being shipped to the browser. The practical rule is the same as with any dependency: measure with your own bundler, and only pull in the caching and extra exchanges or links you genuinely need.

    Do I still need a GraphQL client if I use TanStack Query?▾

    Not always. TanStack Query (with a tiny fetcher like graphql-request) is a perfectly good way to call a GraphQL API when you mainly need request deduplication, caching by query key, and background refetching — and you do not need a GraphQL-aware normalized cache. You should reach for a dedicated GraphQL client like Apollo, urql or Relay when you want normalized caching that automatically updates every query referencing an entity after a mutation, GraphQL-specific features such as fragments, subscriptions and optimistic updates keyed to the schema, or Relay's compiler-driven discipline. If your app is lightly GraphQL and you already use TanStack Query elsewhere, staying with it keeps your stack smaller.

    Which GraphQL client works best with Next.js App Router and Server Components?▾

    All three can be used with the Next.js App Router, but they target it differently. Apollo provides an official experimental integration for the App Router and React Server Components that lets you fetch on the server and hydrate the client cache, and it is the most documented path today. urql is lightweight and framework-agnostic, so it drops into client components easily and pairs well with server-side fetching you wire yourself. Relay works with Next.js but expects its compiler and router conventions, so it is the most involved to set up. Whichever you choose, treat the client as a client-component concern: fetch initial data on the server where you can, hydrate the cache, and keep interactive queries in client components.

    Is Relay worth the extra complexity over Apollo or urql?▾

    Relay is worth it for large, long-lived applications with many engineers and a GraphQL schema you control, where its fragment colocation and compiler pay off as discipline and performance that scale with the codebase. Its model forces each component to declare exactly the data it needs, eliminates over-fetching, and makes data dependencies explicit and reviewable. For smaller apps, early-stage products, or teams new to GraphQL, that same rigor is overhead — the compiler step, the schema requirements and the learning curve slow you down more than they help. If you are unsure, start with urql or Apollo and only migrate to Relay when scale and team size make its guarantees valuable.

    Related guides

    Browse Quality-Scored Code

    Every listing on CodeCudos is analyzed for code quality, security, and documentation. Find production-ready components, templates, and apps — or sell your own code and keep 90%.

    Browse Marketplace →