React vs Vue GraphQL Setup: 13 Steps, 100 Min [2026]

React and Vue have spent the past few years copying each other’s best ideas on routing, state management, and component libraries. GraphQL data fetching is the one corner where the two ecosystems still diverge sharply, and picking the wrong client can cost a team weeks of refactoring once an app outgrows a demo. React developers default to Apollo Client almost by reflex, while Vue developers split between the official Vue Apollo bindings and the framework-agnostic urql library. Neither choice is obviously correct, and the setup steps differ enough that copy-pasting React tutorials into a Vue project simply does not work.

This tutorial walks through building the same GraphQL-powered app twice: once in React 19.2.8 with Apollo Client 4, and once in Vue 3.5 with Vue Apollo, before showing where urql fits as a lighter alternative for either framework. By the end you will have working queries, mutations, subscriptions, generated TypeScript types, and a tested, deployable client layer in both stacks.

The comparisons below focus on the client side only. This is not a tutorial about choosing React over Vue, or about building a GraphQL server from scratch. It assumes you already picked a frontend framework, or maintain both, and now need to wire up the data layer that connects it to a real GraphQL API in a way that survives contact with production traffic, flaky networks, and a schema that keeps evolving after launch.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

Why your GraphQL client choice shapes React and Vue apps in 2026

GraphQL adoption keeps climbing because it lets frontend teams request exactly the fields they need instead of over-fetching REST payloads, and that advantage compounds as apps add more screens and more backend services. But the client library you wrap around a GraphQL endpoint does more work than most REST fetch wrappers ever did. It normalizes the cache, deduplicates in-flight requests, drives loading and error states, and in Apollo’s case even manages local-only fields alongside server data.

React and Vue took different paths here because their reactivity models differ. React re-renders components when state changes through hooks, so Apollo Client’s useQuery and useMutation hooks slot naturally into that model. Vue’s Composition API tracks reactive refs directly, so Vue Apollo’s composables return refs that Vue’s reactivity system watches without any extra subscription plumbing. urql was built framework-agnostic from day one, using a lightweight exchange-based architecture that both React and Vue bindings wrap around the same core.

The stakes are not academic. Apollo Client pulls in a normalized cache, devtools integration, and a mature subscriptions story, but it adds real bundle weight and a steeper learning curve. urql ships a smaller core and a plugin-based “exchanges” system that teams can extend without touching Apollo’s internals. Vue Apollo sits between the two: it wraps Apollo Client’s core so Vue apps get the same cache behavior as React apps, just exposed through composables instead of hooks. Choosing wrong early means either dragging unused Apollo features through your bundle or bolting on caching logic urql never had to begin with.

Prerequisites and versions you need before starting

This tutorial assumes basic familiarity with both React and Vue component syntax and with GraphQL query language fundamentals (queries, mutations, and the general shape of a schema). You do not need prior Apollo or urql experience. Install Node.js 20 or later before you start, since Vite 6 and the current Apollo tooling both expect it.

ToolVersion used in this tutorialPurpose
Node.js20.x or laterRuntime for both project builds
React19.2.8UI framework, hooks-based data layer
Vue3.5.xUI framework, Composition API data layer
@apollo/client4.2.3GraphQL client for React
@vue/apollo-composable4.2.2Vue Apollo composables (Vue 3)
urql / @urql/vuelatest published releaseLightweight alternative GraphQL client
graphql-ws6.0.4WebSocket transport for subscriptions
@graphql-codegen/cli7.1.3Generates TypeScript types from your schema
Vite6.xDev server and bundler for both projects

You will also need a running GraphQL endpoint to query against. If you do not already have one, spin up a quick Apollo Server instance using the steps in our GraphQL Tutorial: Build an Apollo Server API guide, or point these clients at any public GraphQL playground schema while you follow along.

Choosing between Apollo Client, Vue Apollo, and urql before you start

It helps to decide up front rather than midway through Step 6, since ripping out a GraphQL client after subscriptions and generated types are wired in costs far more than picking correctly on day one. Three questions tend to settle it quickly. First, does the app need offline-first behavior or heavy client-side caching across many screens? If so, Apollo’s normalized cache pays for its extra weight. Second, is the team already comfortable with Redux-style middleware and link chains, or would a smaller, more explicit API reduce onboarding time for new hires? Teams that answered the second question with “smaller API” tend to gravitate toward urql. Third, is this a Vue project specifically, and does the team want the exact same cache semantics as a sibling React app on the same backend? That points toward Vue Apollo rather than urql’s Vue bindings, since Vue Apollo shares Apollo Client’s cache engine byte for byte.

None of these choices are permanent. Because GraphQL operations are just strings sent over HTTP or WebSocket, the query and mutation documents you write in Step 3 and Step 5 below will still work if you later swap the client library around them. What changes is the caching, retry, and subscription plumbing, which is exactly what the rest of this tutorial builds step by step.

Step 1: Scaffold your React and Vue projects

Create both projects side by side with Vite so you can compare the setup directly. Vite’s React and Vue templates both ship TypeScript support out of the box, which matters once you get to the code generation step later.

npm create vite@latest react-graphql-app -- --template react-ts
cd react-graphql-app
npm install

npm create vite@latest vue-graphql-app -- --template vue-ts
cd vue-graphql-app
npm install

Run npm run dev in each folder to confirm both scaffolds boot before you add any GraphQL dependencies. This isolates setup problems: if something breaks after the next step, you know it came from the GraphQL client, not the base project.

Step 2: Install and configure Apollo Client in React

Install Apollo Client and the graphql package it depends on. Apollo Client 4 dropped support for the older @apollo/client/core import split, so a single package now covers the client, cache, and React bindings.

npm install @apollo/client graphql

Create the client instance and wrap your app in the provider. Apollo’s InMemoryCache is what handles the normalization work described earlier, storing every entity by its __typename and id so repeated queries for the same object reuse cached data instead of refetching.

// src/apollo-client.ts
import { ApolloClient, InMemoryCache } from '@apollo/client';

export const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache: new InMemoryCache(),
});

// src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { ApolloProvider } from '@apollo/client/react';
import { client } from './apollo-client';
import App from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </StrictMode>,
);

Note that Apollo Client 4 moved the React hooks into a dedicated @apollo/client/react subpath rather than the root package export. This is a common source of confusing “not a function” errors when developers copy Apollo Client 3 tutorials into a version 4 project, since the old imports still resolve but return the wrong bindings.

Step 3: Query and mutate data in React with useQuery and useMutation

With the provider in place, any component under it can call useQuery. Apollo’s hook returns the same three states every React data hook needs: loading, error, and data.

import { useQuery, useMutation, gql } from '@apollo/client/react';

const GET_TASKS = gql`
  query GetTasks {
    tasks {
      id
      title
      completed
    }
  }
`;

const TOGGLE_TASK = gql`
  mutation ToggleTask($id: ID!) {
    toggleTask(id: $id) {
      id
      completed
    }
  }
`;

function TaskList() {
  const { loading, error, data } = useQuery(GET_TASKS);
  const [toggleTask] = useMutation(TOGGLE_TASK);

  if (loading) return <p>Loading tasks...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {data.tasks.map((task: { id: string; title: string; completed: boolean }) => (
        <li key={task.id} onClick={() => toggleTask({ variables: { id: task.id } })}>
          {task.completed ? '✅' : '⬜'} {task.title}
        </li>
      ))}
    </ul>
  );
}

Because the mutation targets a field already present in the cache (the task’s id and completed status), Apollo automatically merges the mutation response back into the normalized cache and re-renders any component reading that task, without you writing manual cache-update code.

Step 4: Install and configure Vue Apollo in Vue 3

Vue Apollo’s composition package wraps the same Apollo Client core used above, so the underlying cache behavior is identical between your React and Vue apps. Install both the composable package and the core client.

npm install @apollo/client @vue/apollo-composable graphql

Vue’s setup looks different from React’s provider pattern because Vue Apollo uses Vue’s built-in provide/inject mechanism instead of a component wrapper, and the injection key comes from the Apollo package itself.

// src/apollo-client.ts
import { ApolloClient, InMemoryCache } from '@apollo/client';

export const apolloClient = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache: new InMemoryCache(),
});

// src/main.ts
import { createApp, provide, h } from 'vue';
import { DefaultApolloClient } from '@vue/apollo-composable';
import { apolloClient } from './apollo-client';
import App from './App.vue';

const app = createApp({
  setup() {
    provide(DefaultApolloClient, apolloClient);
  },
  render: () => h(App),
});

app.mount('#app');

Teams migrating an existing Vue app sometimes skip the wrapper setup() call and try to provide the client directly on the root App.vue instead. That works too, but it means every consuming component has to sit below App.vue in the tree, which is rarely a problem but worth knowing if you use portals or teleports for modals.

Step 5: Query and mutate data in Vue with the Composition API

Vue Apollo’s useQuery and useMutation composables return refs rather than the object Apollo Client’s React hook returns, which is the main mental shift developers coming from React need to make.

<script setup lang="ts">
import { useQuery, useMutation } from '@vue/apollo-composable';
import gql from 'graphql-tag';

const GET_TASKS = gql`
  query GetTasks {
    tasks {
      id
      title
      completed
    }
  }
`;

const TOGGLE_TASK = gql`
  mutation ToggleTask($id: ID!) {
    toggleTask(id: $id) {
      id
      completed
    }
  }
`;

const { result, loading, error } = useQuery(GET_TASKS);
const { mutate: toggleTask } = useMutation(TOGGLE_TASK);
</script>

<template>
  <p v-if="loading">Loading tasks...</p>
  <p v-else-if="error">Error: {{ error.message }}</p>
  <ul v-else>
    <li v-for="task in result.tasks" :key="task.id" @click="toggleTask({ id: task.id })">
      {{ task.completed ? '✅' : '⬜' }} {{ task.title }}
    </li>
  </ul>
</template>

The result ref starts out as undefined before the first response arrives, which is why the template guards on loading and error before touching result.tasks. Forgetting that guard is one of the most common runtime errors new Vue Apollo users hit, since Vue’s template compiler will not catch the missing null check at build time the way a strict TypeScript React component would.

Step 6: Add real-time subscriptions with graphql-ws

Both Apollo setups need a split link to route subscription operations over a WebSocket while queries and mutations stay on HTTP. The graphql-ws library implements the modern GraphQL-over-WebSocket protocol that has replaced the older subscriptions-transport-ws package in most current backends.

npm install graphql-ws
// shared setup, works the same in React and Vue client files
import { ApolloClient, InMemoryCache, HttpLink, split } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';

const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' });

const wsLink = new GraphQLWsLink(
  createClient({ url: 'ws://localhost:4000/graphql' }),
);

const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink,
);

export const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache(),
});

Drop this client into either the React ApolloProvider or the Vue provide(DefaultApolloClient, ...) call from earlier steps and both frameworks gain subscription support with zero changes to the query and mutation code you already wrote. Call useSubscription from either the React or Vue Apollo package the same way you called useQuery.

Step 7: Generate TypeScript types with GraphQL Code Generator

Hand-typing the shape of every query response gets tedious fast, and it drifts out of sync with the schema the moment a backend field gets renamed. GraphQL Code Generator reads your schema and your .graphql operation files, then emits typed hooks for both React and Vue automatically.

npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
# codegen.yml
schema: http://localhost:4000/graphql
documents: 'src/**/*.graphql'
generates:
  src/generated/graphql.tsx:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo

For the Vue project, swap the last plugin for typescript-vue-apollo, which the GraphQL Code Generator plugin ecosystem maintains as a separate package targeting Vue Apollo’s composable signatures. Run npx graphql-codegen after any schema or query change, or wire it into a --watch script during development so generated types never fall behind the live schema.

Step 8: Try urql as a lighter-weight alternative

urql targets teams who find Apollo’s cache and link system more machinery than they need. Its core ships as a much smaller dependency, and its default “document cache” only caches by exact query and variables rather than fully normalizing every entity, which is simpler to reason about for apps with straightforward data shapes.

# React
npm install urql graphql

# Vue
npm install @urql/vue graphql
// React setup
import { Client, cacheExchange, fetchExchange, Provider, useQuery } from 'urql';

const client = new Client({
  url: 'http://localhost:4000/graphql',
  exchanges: [cacheExchange, fetchExchange],
});

function App() {
  return (
    <Provider value={client}>
      <TaskList />
    </Provider>
  );
}

function TaskList() {
  const [result] = useQuery({ query: GET_TASKS });
  const { data, fetching, error } = result;
  if (fetching) return <p>Loading...</p>;
  if (error) return <p>{error.message}</p>;
  return <ul>{data.tasks.map((t: any) => <li key={t.id}>{t.title}</li>)}</ul>;
}

The Vue binding, @urql/vue, mirrors Vue Apollo’s composable pattern closely enough that switching between the two mid-project mostly means renaming imports and adjusting how you read the reactive result object. That portability is part of urql’s appeal for teams who maintain both a React and a Vue codebase and want the two data layers to look as similar as possible.

Step 9: Cache normalization, optimistic UI, and pagination

Apollo’s InMemoryCache supports optimistic responses out of the box, which lets a mutation update the UI immediately while the network request is still in flight, then reconcile with the real server response when it arrives.

toggleTask({
  variables: { id: task.id },
  optimisticResponse: {
    toggleTask: { __typename: 'Task', id: task.id, completed: !task.completed },
  },
});

This snippet works identically whether you call it from React’s useMutation or Vue Apollo’s mutate function, because both sit on top of the same underlying cache. For paginated lists, Apollo’s fetchMore and field policies handle merging new pages into an existing cached array, while urql leans on its @urql/exchange-graphcache add-on to get comparable normalized-cache behavior, since urql’s default document cache does not merge paginated results automatically the way Apollo does.

Step 10: Error handling, retries, and auth headers

Production apps need to attach an auth token to every request and retry transient network failures without duplicating that logic in every component. Apollo does this through a link chain, adding a small middleware link before the HTTP link.

import { ApolloLink } from '@apollo/client';
import { RetryLink } from '@apollo/client/link/retry';

const authLink = new ApolloLink((operation, forward) => {
  operation.setContext(({ headers = {} }) => ({
    headers: {
      ...headers,
      authorization: `Bearer ${localStorage.getItem('token')}`,
    },
  }));
  return forward(operation);
});

const retryLink = new RetryLink({
  attempts: { max: 3 },
});

Chain authLink, retryLink, and your existing splitLink together with Apollo’s from([...]) helper, and both the React and Vue clients pick up the same auth and retry behavior. urql’s equivalent is an authExchange from the @urql/exchange-auth package, which slots into the exchanges array shown in Step 8 in a similar left-to-right order.

Step 11: Testing your GraphQL layer in both frameworks

Apollo ships a MockedProvider component specifically for testing components that call useQuery or useMutation without hitting a real network. Pair it with React Testing Library and Vitest for a fast, isolated test.

import { render, screen } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { GET_TASKS } from './queries';
import TaskList from './TaskList';

const mocks = [
  {
    request: { query: GET_TASKS },
    result: { data: { tasks: [{ id: '1', title: 'Write tests', completed: false }] } },
  },
];

test('renders tasks from a mocked query', async () => {
  render(
    <MockedProvider mocks={mocks} addTypename={false}>
      <TaskList />
    </MockedProvider>,
  );
  expect(await screen.findByText('Write tests')).toBeInTheDocument();
});

Vue Apollo does not ship an equivalent mocked provider, so most Vue teams either mock the injected Apollo client directly with Vitest’s vi.mock, or stand up a lightweight schema mock with a tool like graphql-tools‘ schema mocking utilities and point a real Apollo Client at it during tests. Our React vs Vue Testing: Vitest vs Jest guide covers the broader Vitest and Vue Test Utils setup this section assumes.

Step 12: Deploying and monitoring in production

Both client setups build to static assets through Vite, so deployment itself does not change. What does change in production is how you handle the WebSocket URL for subscriptions (it needs to switch from ws:// to wss:// behind TLS) and how aggressively you persist Apollo’s cache.

  • Read the GraphQL endpoint and WebSocket URL from environment variables rather than hardcoding localhost, using Vite’s import.meta.env convention in both React and Vue projects.
  • Enable Apollo’s persisted queries link if your backend supports automatic persisted queries, which cuts request payload size for repeat operations.
  • Add the Apollo Client devtools browser extension during staging QA, since it inspects the normalized cache directly and catches merge bugs before they reach users.
  • Log GraphQL errors (not just network errors) to your monitoring tool, since a 200 response with a populated errors array is easy to miss if you only watch HTTP status codes.

Migrating an existing REST-based React or Vue app incrementally

Few teams get to start a GraphQL client on a blank project. Most add one to an app that already has REST fetch calls scattered across dozens of components, and rewriting everything at once is rarely realistic. Both Apollo Client and urql handle this well because neither requires you to route every request through them from day one. Install the client, wrap the root component in the provider as shown in Step 2 or Step 4, and then convert one screen at a time, leaving untouched REST calls exactly where they are.

A useful order of operations is to migrate read-heavy screens first, since useQuery immediately replaces a manual fetch plus useState plus useEffect combination with less code and automatic caching. Save mutation-heavy forms for later, once the team is comfortable with how the normalized cache updates after a write. If your backend does not yet expose a GraphQL endpoint at all, a schema-stitching gateway can wrap your existing REST endpoints in a GraphQL layer without touching the underlying services, letting the frontend migration happen well ahead of any backend rewrite.

Apollo Client vs Vue Apollo vs urql: feature and performance comparison

Weekly npm download figures make the popularity gap clear. Apollo Client sits at roughly 2 million weekly downloads, well ahead of urql’s roughly 600,000, though the ordering depends heavily on which month and dataset you check since both libraries see real month-to-month swings. React itself dwarfs both frameworks it serves, at roughly 50 million weekly downloads against Vue’s roughly 7 million, a gap that mirrors the State of JS 2025 survey’s usage figures of 83.6% for React versus 52% for Vue, even though Vue scored higher on developer satisfaction at 84% versus React’s 72%.

FactorApollo Client (React)Vue Apollourql
Latest version (2026)4.2.34.2.2 (@vue/apollo-composable)latest published release
Bundle footprintLarger, full-featuredSame core as Apollo ClientSmaller core, modular exchanges
Cache modelNormalized (InMemoryCache)Normalized (shared with Apollo)Document cache by default; normalized via graphcache exchange
Subscriptions supportBuilt-in via graphql-ws linkBuilt-in via same linkBuilt-in subscriptionExchange
Official framework bindingYes, first-partyYes, community-maintained under the Apollo GraphQL orgYes, first-party for both React and Vue
Approx. weekly npm downloads~2M+Included in Apollo Client core downloads~600K+
Best fitLarge apps needing rich caching and devtoolsVue teams wanting Apollo’s cache behaviorTeams wanting a lean, framework-portable client

None of these numbers should be read as a verdict on code quality. urql’s smaller download count reflects a smaller, more opinionated niche rather than technical weakness, and Vue Apollo’s download count is folded into Apollo Client’s own npm statistics since it depends on the same core package rather than shipping a competing cache implementation.

5 common pitfalls when wiring up a GraphQL client

  • Importing Apollo Client 3 syntax into a version 4 project. The React hooks moved to @apollo/client/react in version 4, and root-level imports that used to work silently resolve to the wrong module or fail entirely.
  • Forgetting the loading and error guards in Vue templates. Vue Apollo’s result ref is undefined before the first response, and accessing a nested field on it without a v-if guard throws a runtime error the compiler will not catch.
  • Mixing up cache identity across frameworks. If your React and Vue apps hit the same backend but define GraphQL types with different __typename expectations, Apollo’s normalized cache in one app can silently diverge from the other’s assumptions about entity identity.
  • Skipping the split link for subscriptions. Sending a subscription operation straight to an HTTP link fails silently in some setups and throws a cryptic protocol error in others, because HTTP links have no concept of a persistent connection.
  • Letting generated types drift from the schema. Running GraphQL Code Generator once and never again means a renamed backend field breaks production without a single compile error, since the generated types were correct only at the moment you last ran codegen.

Troubleshooting: 8 errors you’ll hit and how to fix them

1. “useQuery is not a function” in Apollo Client 4. You imported from @apollo/client instead of @apollo/client/react. Update the import path.

2. “Cannot read properties of undefined (reading ‘tasks’)” in Vue. The template accessed result.tasks before the query resolved. Add a v-if="loading" branch before the branch that reads result.

3. Subscriptions connect then immediately close. Your backend likely still expects the older subscriptions-transport-ws protocol. Confirm the server implements the newer graphql-ws protocol, or fall back to a compatible link if you cannot upgrade the backend yet.

4. Mutation succeeds but the UI does not update. The mutation response is missing a field the cache needs to identify the entity, usually id or __typename. Add both to every mutation’s return selection.

5. GraphQL Code Generator throws a schema fetch error. The dev server in codegen.yml was not running when you executed the generator. Start your GraphQL server first, or point schema at a local .graphql SDL file instead of a live URL.

6. urql query fires twice on mount in React StrictMode. This is expected StrictMode behavior in development, not a bug in urql or your exchanges configuration. It does not happen in production builds.

7. CORS errors only on subscription connections. WebSocket connections need their own CORS-equivalent handling on most GraphQL servers, separate from the HTTP endpoint’s CORS configuration. Check your server’s WebSocket-specific origin allowlist.

8. Optimistic UI flickers back to the old value. The optimisticResponse object’s shape does not exactly match what the real mutation returns, so Apollo replaces the optimistic entry with a response that looks like a rollback. Make sure every field in optimisticResponse matches the mutation’s actual selection set.

Advanced tips for production-grade GraphQL apps

Once the basic query, mutation, and subscription flows work, a few refinements separate a demo from a production-ready data layer. Set explicit fetchPolicy values per query rather than relying on Apollo’s default cache-first behavior everywhere. A dashboard that must show fresh data on every visit benefits from cache-and-network, while a rarely changing settings screen is a good fit for cache-first.

Use GraphQL fragments to share field selections between related queries instead of duplicating field lists, which also lets Apollo’s cache update every query that references a fragment when just one of them refetches. For teams running both a React and a Vue client against the same backend, keep the .graphql operation files in a shared package so both codegen configs draw from a single source of truth for query strings, cutting the risk of the two frontends drifting into subtly different field selections over time.

If bundle size becomes a concern in the React app, Apollo Client 4’s modular imports let you exclude devtools and unused link types from the production build. urql users get a similar win almost for free, since its exchange-based architecture already tree-shakes unused exchanges by default.

Watch the backend side of the equation too. A GraphQL client can only be as fast as the resolvers behind it, and a schema that lets a single query trigger dozens of database round trips (the classic N+1 problem) will make even a perfectly configured Apollo or urql client feel slow. If your team also owns the backend, pair this client-side setup with a DataLoader-style batching layer on the server so a query for ten tasks and their owners issues one batched database call instead of eleven separate ones.

Larger organizations running several backend teams behind one API often adopt schema federation, where multiple services each own a slice of a combined GraphQL schema. Neither Apollo Client, Vue Apollo, nor urql needs to know or care that the schema is federated. All three send the same query documents to a single gateway URL exactly as shown in this tutorial, and the gateway handles routing sub-queries to the right backend service behind the scenes. That separation is one of the practical reasons GraphQL adoption keeps growing at large companies even as REST remains simpler for small, single-service APIs.

Complete working project recap

Following all twelve steps above leaves you with two working Vite projects, a React app on Apollo Client 4 and a Vue app on Vue Apollo, both pointed at the same GraphQL schema. Each project queries a task list, toggles completion through a mutation with an optimistic update, receives live updates through a graphql-ws subscription, and ships fully typed thanks to GraphQL Code Generator. You also have a third, optional urql-based variant of either app for teams that want to compare bundle size and cache behavior directly before committing to one client library.

Sample output when the React app first loads the task list, before any interaction:

⬜ Write tests
⬜ Ship the GraphQL client
⬜ Review the PR

Clicking a task fires the mutation, updates optimistically, and the checkbox flips before the network round trip completes:

✅ Write tests
⬜ Ship the GraphQL client
⬜ Review the PR

The Vue app produces identical output through its own template, confirming that the two frameworks’ GraphQL layers behave the same way from a user’s perspective, even though the code underneath uses hooks in one case and composables in the other. For a deeper look at how each framework’s reactivity model handles derived and cached state outside of GraphQL specifically, see our React vs Vue State Management and React vs Vue Data Fetching guides, which cover Redux Toolkit, Pinia, TanStack Query, and Pinia Colada as REST-oriented or client-state alternatives to the GraphQL-specific tools in this tutorial.

Frequently asked questions

Is Apollo Client or urql better for a new React project in 2026?

Apollo Client remains the stronger default for larger apps that need a fully normalized cache, mature devtools, and broad community support, reflected in its lead of roughly 2 million weekly npm downloads against urql’s roughly 600,000. urql fits teams that want a smaller dependency footprint and are comfortable adding exchanges piece by piece as requirements grow.

Does Vue Apollo work with Vue 3’s Composition API?

Yes. The @vue/apollo-composable package, at version 4.2.2, is built specifically for the Composition API and exposes composables like useQuery and useMutation that return reactive refs rather than options-API mixins.

Can I use the same GraphQL schema for both a React and a Vue frontend?

Yes, and it is common practice. Both Apollo Client and urql are transport-agnostic on the client side, meaning any standards-compliant GraphQL server works with either framework’s client library without server-side changes.

Do I need GraphQL Code Generator, or can I write types by hand?

You can write types manually on a small project, but GraphQL Code Generator, currently at version 7.1.3, removes an entire class of bugs where a query’s TypeScript type no longer matches the actual schema after a backend change. Most teams add it as soon as the schema stabilizes past the prototype stage.

Why does my subscription fail even though queries and mutations work fine?

Subscriptions require a separate WebSocket transport handled by graphql-ws, currently at version 6.0.4, routed through a split link based on operation type. If you only configured an HTTP link, subscriptions have no transport to run on and will fail immediately or hang.

Is it hard to switch from Apollo Client to urql later, or vice versa?

The query and mutation strings themselves are portable since both clients accept standard GraphQL documents. The migration work is mostly in cache configuration and any Apollo-specific features like field policies or local-only fields, which do not have a direct urql equivalent and need to be rebuilt using urql’s graphcache exchange.

Does React’s larger ecosystem mean better GraphQL tooling than Vue?

Not necessarily for GraphQL specifically. Because Vue Apollo wraps the same Apollo Client core that React uses, Vue apps get the identical cache engine, and GraphQL Code Generator supports both frameworks with dedicated plugins. React’s edge shows up more in adjacent tooling and community example count than in the GraphQL client’s core capability.

What GraphQL server works best for testing this setup locally?

Apollo Server is the most common pairing since it is maintained by the same organization as Apollo Client and supports subscriptions and persisted queries natively. Our GraphQL Tutorial: Build an Apollo Server API guide walks through standing one up in 13 steps if you need a backend to test against before wiring up either frontend client.

Related Coverage

Sofia Lindström

Sofia Lindström

Editor-in-Chief

Sofia Lindström is the Editor-in-Chief at Tech Insider, where she leads editorial strategy and oversees coverage across AI, cybersecurity, and enterprise technology. With over a decade in Swedish tech journalism, she previously served as technology editor at Dagens Industri and covered the Nordic startup ecosystem for Breakit. Sofia holds an MSc in Media Technology from KTH Royal Institute of Technology and is a frequent speaker at Web Summit and Slush. She is passionate about making complex technology accessible to business leaders.

View all articles