Skip to content
Home
React with GraphQL: Apollo Client, Queries, Mutations, and...

React with GraphQL: Apollo Client, Queries, Mutations, and...

React React 8 min read 1552 words Beginner ExcellentWiki Editorial Team

GraphQL offers a more efficient alternative to REST by letting clients request exactly the data they need. Apollo Client is the leading GraphQL client for React, providing hooks for queries, mutations, and subscriptions with built-in caching and state management.

Setting Up Apollo Client

Install the required packages:

npm install @apollo/client graphql

Create an Apollo Client instance and wrap your app with the provider:

import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://api.example.com/graphql',
  cache: new InMemoryCache(),
---);

function App() {
  return (
    <ApolloProvider client={client}>
      <MainContent />
    </ApolloProvider>
  );
---

useQuery Hook

The useQuery hook fetches data declaratively — it returns loading, error, and data states automatically.

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

const GET_BOOKS = gql`
  query GetBooks {
    books {
      id
      title
      author
      price
    }
  }
`;

function BookList() {
  const { loading, error, data } = useQuery(GET_BOOKS);

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

  return (
    <ul>
      {data.books.map(book => (
        <li key={book.id}>{book.title} by {book.author}</li>
      ))}
    </ul>
  );
---

Query Variables

Pass variables to queries for dynamic data fetching:

const GET_BOOK = gql`
  query GetBook($id: ID!) {
    book(id: $id) {
      id
      title
      author
      description
    }
  }
`;

function BookDetail({ bookId }) {
  const { loading, data } = useQuery(GET_BOOK, {
    variables: { id: bookId },
  });
  // ...
---

useMutation Hook

Mutations modify server-side data. Apollo’s useMutation provides an update function to modify the cache after a mutation succeeds.

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

const ADD_BOOK = gql`
  mutation AddBook($title: String!, $author: String!) {
    addBook(title: $title, author: $author) {
      id
      title
      author
    }
  }
`;

function AddBookForm() {
  const [addBook, { loading, error }] = useMutation(ADD_BOOK, {
    update(cache, { data: { addBook } }) {
      const existing = cache.readQuery({ query: GET_BOOKS });
      cache.writeQuery({
        query: GET_BOOKS,
        data: { books: [...existing.books, addBook] },
      });
    },
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    addBook({ variables: { title, author } });
  };
  // ...
---

Optimistic Updates

Optimistic updates update the UI immediately before the server confirms the mutation, providing a snappy user experience:

const [addBook] = useMutation(ADD_BOOK, {
  optimisticResponse: {
    addBook: {
      id: `temp-${Date.now()}`,
      title: optimisticTitle,
      author: optimisticAuthor,
      __typename: 'Book',
    },
  },
  update: (cache, { data }) => { /* cache update */ },
---);

Caching

Apollo’s normalized cache automatically deduplicates entities by combining __typename with id or _id to form cache keys. Configure type policies for custom cache behavior:

const client = new ApolloClient({
  uri: '/graphql',
  cache: new InMemoryCache({
    typePolicies: {
      Book: {
        keyFields: ['isbn'],  // use 'isbn' instead of 'id'
      },
      Query: {
        fields: {
          books: {
            merge(existing = [], incoming) {
              return [...existing, ...incoming];
            },
          },
        },
      },
    },
  }),
---);

Pagination with GraphQL

Implement cursor-based pagination using the Relay connection specification with fetchMore:

const GET_BOOKS = gql`
  query GetBooks($cursor: String) {
    books(first: 10, after: $cursor) {
      edges {
        node {
          id
          title
        }
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
`;

function BookList() {
  const { loading, data, fetchMore } = useQuery(GET_BOOKS);

  const loadMore = () => {
    fetchMore({
      variables: { cursor: data.books.pageInfo.endCursor },
    });
  };

  return (
    <div>
      {data?.books.edges.map(({ node }) => (
        <div key={node.id}>{node.title}</div>
      ))}
      {data?.books.pageInfo.hasNextPage && (
        <button onClick={loadMore}>Load More</button>
      )}
    </div>
  );
---

Subscriptions

Subscriptions push real-time updates from the server. Set up a WebSocket link in addition to the HTTP link:

import { split, HttpLink } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';

const wsLink = new GraphQLWsLink(createClient({
  url: 'ws://api.example.com/graphql',
---));

const httpLink = new HttpLink({ uri: 'https://api.example.com/graphql' });

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

Pagination Strategies

For list views, cursor-based pagination is preferred over offset-based pagination. Apollo’s fetchMore function combines new results with existing cached data. Define a typePolicy with custom merge function for the connection type: { keyArgs: false, merge(existing = [], incoming) { return [...existing, ...incoming] } }. Relay-style pagination uses @connection directives and provides standardized pagination arguments (first, after, last, before). For infinite scroll, call fetchMore when the user scrolls near the bottom of the list. Always show loading indicators for subsequent pages and handle the case where no more pages exist.

Summary

Apollo Client integrates GraphQL with React through declarative hooks, automatic caching, and optimistic updates. Use useQuery for data fetching, useMutation for data modification with cache updates, and fetchMore for pagination. Configure type policies for custom cache behavior. Subscriptions enable real-time features through WebSocket connections.

Error Handling and Loading States

Every query should handle loading, error, and success states. Apollo’s useQuery returns loading, error, and data properties. Show a spinner during loading, an error message with retry button on failure, and the content on success. For mutations, use onCompleted and onError callbacks in useMutation. Implement global error handling with Apollo Link’s onError link — log errors to your monitoring service and show toast notifications. Use error boundaries in React to catch rendering errors from malformed data. This comprehensive error handling ensures a robust user experience even when the GraphQL backend has issues.

FAQ

How does Apollo caching work?

Apollo’s InMemoryCache normalizes every object by combining __typename with id (or a custom key). When the same object appears in multiple queries, Apollo deduplicates it — updates to one query automatically reflect in all others that reference the same object. Configure typePolicies for custom merge logic on paginated fields.

Should I use Apollo Client or urql?

Apollo Client has richer caching, a larger ecosystem, and better documentation. urql is lighter (5x smaller bundle) and simpler for basic use cases. Choose Apollo for complex applications with aggressive caching needs; choose urql for smaller projects where bundle size matters.

How do I handle authentication with Apollo?

Use Apollo Link middleware to attach auth tokens to every request: import { setContext } from '@apollo/client/link/context'; then create an auth link that reads the token from localStorage and adds it as an Authorization header. Handle 401 responses by redirecting to login.

Optimistic Updates and UI Responsiveness

Optimistic updates apply the expected mutation result to the Apollo cache immediately, then revert if the server returns an error. This creates a snappy user experience even on slow connections. Use the optimisticResponse option in useMutation: the cache updates right away, and when the server responds, the cache is reconciled with the actual result. Handle errors with onError or try-catch, and use the update function to revert optimistic data. For list mutations, update cached queries directly by reading and writing the cache with cache.readQuery and cache.writeQuery.

Using Fragments for Reusable Fields

GraphQL fragments define reusable field selections that keep queries DRY. Define fragments on types: fragment UserFields on User { id name email avatar }. Include fragments in queries with spread syntax: query GetUser { user { ...UserFields } }. Apollo Client resolves fragments automatically. Fragments also enable cache updates by matching on __typename and key fields. Use fragment matchers in typePolicies for union and interface types to ensure correct cache normalization.

Testing GraphQL Components

Test GraphQL components by mocking the Apollo client. Use MockedProvider from @apollo/client/testing to provide mock query and mutation responses. Define mocks that match the exact query document and variables your component uses. Test loading, error, and success states by controlling whether the mock resolves or rejects. For mutations, verify the component calls the mutation with correct variables and handles the response. Mocking the entire GraphQL layer makes tests deterministic and fast while still exercising your component’s data-fetching logic.

Advanced Cache Configuration

Apollo’s InMemoryCache supports fine-grained control over data normalization and garbage collection. Use typePolicies to define custom key fields, merge strategies for paginated lists, and read functions for computed fields. The cache.gc() method triggers garbage collection of unreferenced objects. Use evict and modify methods for targeted cache updates without refetching. Configure possibleTypes for union and interface types to enable proper cache normalization across polymorphic types. These advanced cache features prevent stale data and optimize performance in complex applications.

Using Apollo Client DevTools

The Apollo Client DevTools browser extension provides real-time insight into cache state, active queries, and mutation history. Inspect the cache contents to verify data normalization and cross-query deduplication. Use the GraphQL query tab to test queries directly against your endpoint. Track loading, error, and data states for each active query. The Watched Queries view shows which components are subscribed to each query. DevTools are essential for debugging cache behavior, identifying unnecessary refetches, and optimizing your GraphQL data layer during development.

Optimizing Bundle Size with Apollo

Apollo Client’s modular architecture lets you import only the features you use. Import @apollo/client/core instead of @apollo/client to exclude the React integration if you use Apollo outside React. Use tree-shaking-friendly imports: import { gql } from '@apollo/client' instead of namespace imports. Skip @apollo/client/utilities imports for production. Apollo’s cache can be lazy-loaded after initial render. These optimizations reduce bundle size by 30-50% for large applications.

What is the difference between queries and mutations in Apollo?

Queries fetch read-only data and are cached automatically. Mutations modify data and require explicit cache updates. Queries retry on error; mutations return a promise you can handle programmatically. Both use the same gql template literal syntax for defining operations.

For a comprehensive overview, read our article on Next Js Guide.

For a comprehensive overview, read our article on React Authentication.

Section: React 1552 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top