DevToolBoxZA DARMO
Blog

GraphQL Apollo: Tutorial React

14 minby DevToolBox

GraphQL has transformed how frontend applications fetch data from APIs. Instead of multiple REST endpoints returning fixed data shapes, GraphQL lets you request exactly the data you need in a single query. Apollo Client is the most popular GraphQL client for React, providing caching, state management, and developer tools out of the box. This tutorial walks you through building a complete React application with Apollo Client from scratch.

What Is GraphQL?

GraphQL is a query language for APIs and a runtime for executing those queries. Developed by Facebook in 2012 and open-sourced in 2015, it provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need, and makes it easier to evolve APIs over time.

Unlike REST, where each endpoint returns a fixed data structure, GraphQL exposes a single endpoint. The client sends a query describing the exact shape of data it needs, and the server responds with precisely that shape. This eliminates over-fetching (getting more data than needed) and under-fetching (needing multiple requests to assemble the required data).

GraphQL vs REST: Key Differences

AspectRESTGraphQL
EndpointsMultiple (GET /users, GET /posts)Single (/graphql)
Data FetchingFixed response shapeClient specifies exact fields
Over-fetchingCommonEliminated
Under-fetchingRequires multiple requestsSingle query gets all data
VersioningURL versioning (v1, v2)Schema evolution, no versioning needed
CachingHTTP caching built-inRequires client-side cache (Apollo)
ToolingMature (Swagger, Postman)Strong (GraphiQL, Apollo DevTools)
Learning CurveLowMedium

Setting Up Apollo Client in React

Apollo Client provides everything you need to manage GraphQL data in a React application: a declarative data fetching API, an intelligent normalized cache, and excellent developer tools.

// 1. Install dependencies
// npm install @apollo/client graphql

// 2. src/lib/apollo-client.ts
import {
  ApolloClient,
  InMemoryCache,
  createHttpLink,
  from,
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';

// HTTP connection to the API
const httpLink = createHttpLink({
  uri: process.env.NEXT_PUBLIC_GRAPHQL_URL || 'http://localhost:4000/graphql',
});

// Authentication middleware
const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem('auth_token');
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : '',
    },
  };
});

// Error handling
const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path }) => {
      console.error(
        `[GraphQL error]: Message: ${message}, Path: ${path}`
      );
    });
  }
  if (networkError) {
    console.error(`[Network error]: ${networkError}`);
  }
});

// Create the Apollo Client instance
export const client = new ApolloClient({
  link: from([errorLink, authLink, httpLink]),
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          posts: {
            keyArgs: ['filter'],
            merge(existing = [], incoming) {
              return [...existing, ...incoming];
            },
          },
        },
      },
    },
  }),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'cache-and-network',
    },
  },
});

// 3. src/app/providers.tsx
'use client';
import { ApolloProvider } from '@apollo/client';
import { client } from '@/lib/apollo-client';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ApolloProvider client={client}>
      {children}
    </ApolloProvider>
  );
}

Writing GraphQL Queries

GraphQL queries describe the exact data shape you need. Apollo Client provides the useQuery hook that handles loading states, errors, and caching automatically.

// src/graphql/queries.ts
import { gql } from '@apollo/client';

// Fragment for reusable fields
export const USER_FIELDS = gql`
  fragment UserFields on User {
    id
    name
    email
    avatar
    role
  }
`;

export const POST_FIELDS = gql`
  fragment PostFields on Post {
    id
    title
    slug
    excerpt
    content
    publishedAt
    tags
    author {
      ...UserFields
    }
  }
  ${USER_FIELDS}
`;

// Query: Get all posts with pagination
export const GET_POSTS = gql`
  query GetPosts($limit: Int!, $offset: Int!, $filter: PostFilter) {
    posts(limit: $limit, offset: $offset, filter: $filter) {
      ...PostFields
    }
    postsCount(filter: $filter)
  }
  ${POST_FIELDS}
`;

// Query: Get single post by slug
export const GET_POST = gql`
  query GetPost($slug: String!) {
    post(slug: $slug) {
      ...PostFields
      comments {
        id
        body
        createdAt
        author {
          ...UserFields
        }
      }
    }
  }
  ${POST_FIELDS}
`;

// Query: Get current user
export const GET_ME = gql`
  query GetMe {
    me {
      ...UserFields
      posts {
        id
        title
        publishedAt
      }
    }
  }
  ${USER_FIELDS}
`;

Using useQuery in Components

The useQuery hook is the primary way to fetch data with Apollo Client. It returns loading, error, and data states, plus refetch and fetchMore functions for pagination.

// src/components/PostList.tsx
'use client';
import { useQuery } from '@apollo/client';
import { GET_POSTS } from '@/graphql/queries';

interface Post {
  id: string;
  title: string;
  slug: string;
  excerpt: string;
  publishedAt: string;
  author: { name: string; avatar: string };
  tags: string[];
}

const POSTS_PER_PAGE = 10;

export function PostList() {
  const { loading, error, data, fetchMore } = useQuery(GET_POSTS, {
    variables: {
      limit: POSTS_PER_PAGE,
      offset: 0,
      filter: { published: true },
    },
    notifyOnNetworkStatusChange: true,
  });

  if (error) return <div>Error: {error.message}</div>;
  if (loading && !data) return <div>Loading posts...</div>;

  const { posts, postsCount } = data;
  const hasMore = posts.length < postsCount;

  const loadMore = () => {
    fetchMore({
      variables: { offset: posts.length },
    });
  };

  return (
    <div>
      {posts.map((post: Post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
          <div>
            <img src={post.author.avatar} alt={post.author.name} />
            <span>{post.author.name}</span>
            <time>{new Date(post.publishedAt).toLocaleDateString()}</time>
          </div>
          <div>
            {post.tags.map(tag => (
              <span key={tag}>{tag}</span>
            ))}
          </div>
        </article>
      ))}
      {hasMore && (
        <button onClick={loadMore} disabled={loading}>
          {loading ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}

Mutations: Creating and Updating Data

Mutations are GraphQL operations that modify server-side data. Apollo Client provides the useMutation hook with optimistic updates, cache manipulation, and automatic refetching capabilities.

// src/graphql/mutations.ts
import { gql } from '@apollo/client';
import { POST_FIELDS } from './queries';

export const CREATE_POST = gql`
  mutation CreatePost($input: CreatePostInput!) {
    createPost(input: $input) {
      ...PostFields
    }
  }
  ${POST_FIELDS}
`;

export const UPDATE_POST = gql`
  mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
    updatePost(id: $id, input: $input) {
      ...PostFields
    }
  }
  ${POST_FIELDS}
`;

export const DELETE_POST = gql`
  mutation DeletePost($id: ID!) {
    deletePost(id: $id) {
      id
    }
  }
`;

export const ADD_COMMENT = gql`
  mutation AddComment($postId: ID!, $body: String!) {
    addComment(postId: $postId, body: $body) {
      id
      body
      createdAt
      author {
        id
        name
        avatar
      }
    }
  }
`;

// src/components/CreatePostForm.tsx
'use client';
import { useState } from 'react';
import { useMutation } from '@apollo/client';
import { CREATE_POST } from '@/graphql/mutations';
import { GET_POSTS } from '@/graphql/queries';

export function CreatePostForm() {
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');
  const [tags, setTags] = useState<string[]>([]);

  const [createPost, { loading, error }] = useMutation(CREATE_POST, {
    // Update the cache after mutation
    update(cache, { data: { createPost: newPost } }) {
      cache.modify({
        fields: {
          posts(existingPosts = []) {
            const newPostRef = cache.writeFragment({
              data: newPost,
              fragment: POST_FIELDS,
            });
            return [newPostRef, ...existingPosts];
          },
          postsCount(existing) {
            return existing + 1;
          },
        },
      });
    },
    // Optimistic response for instant UI feedback
    optimisticResponse: {
      createPost: {
        __typename: 'Post',
        id: 'temp-id',
        title,
        slug: title.toLowerCase().replace(/\s+/g, '-'),
        excerpt: content.substring(0, 160),
        content,
        publishedAt: new Date().toISOString(),
        tags,
        author: {
          __typename: 'User',
          id: 'current-user',
          name: 'You',
          email: '',
          avatar: '/default-avatar.png',
          role: 'AUTHOR',
        },
      },
    },
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      await createPost({
        variables: { input: { title, content, tags } },
      });
      setTitle('');
      setContent('');
      setTags([]);
    } catch (err) {
      console.error('Failed to create post:', err);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={title}
        onChange={e => setTitle(e.target.value)}
        placeholder="Post title"
        required
      />
      <textarea
        value={content}
        onChange={e => setContent(e.target.value)}
        placeholder="Write your post..."
        required
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Creating...' : 'Create Post'}
      </button>
      {error && <p>Error: {error.message}</p>}
    </form>
  );
}

Apollo Cache: How It Works

Apollo Client uses a normalized, in-memory cache called InMemoryCache. When a query returns data, Apollo breaks the response into individual objects, identifies each by its __typename and id, and stores them in a flat lookup table. If two queries return the same object (same __typename + id), Apollo stores it only once and updates all components referencing it.

This normalization means that when you update a post through a mutation, every component displaying that post automatically re-renders with the new data. No manual state synchronization needed.

// Cache configuration with type policies
const cache = new InMemoryCache({
  typePolicies: {
    // Customize how Post objects are identified
    Post: {
      keyFields: ['slug'], // Use slug instead of id as cache key
    },

    // Pagination: merge incoming results with existing
    Query: {
      fields: {
        posts: {
          keyArgs: ['filter'], // Separate cache entries per filter
          merge(existing = [], incoming, { args }) {
            const offset = args?.offset || 0;
            const merged = existing.slice(0);
            for (let i = 0; i < incoming.length; i++) {
              merged[offset + i] = incoming[i];
            }
            return merged;
          },
          read(existing, { args }) {
            const offset = args?.offset || 0;
            const limit = args?.limit || existing?.length;
            return existing?.slice(offset, offset + limit);
          },
        },
      },
    },
  },
});

// Reading from cache directly
const post = client.readFragment({
  id: 'Post:my-post-slug',
  fragment: gql`
    fragment CachedPost on Post {
      id
      title
      content
    }
  `,
});

// Writing to cache directly
client.writeFragment({
  id: 'Post:my-post-slug',
  fragment: gql`
    fragment UpdatedPost on Post {
      title
    }
  `,
  data: { title: 'Updated Title' },
});

// Evicting from cache
client.cache.evict({ id: 'Post:my-post-slug' });
client.cache.gc(); // Garbage collect unreachable refs

Real-Time Data with Subscriptions

GraphQL subscriptions enable real-time updates through WebSocket connections. Apollo Client supports subscriptions, letting your UI automatically update when server-side data changes.

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

const wsLink = new GraphQLWsLink(
  createClient({
    url: 'ws://localhost:4000/graphql',
    connectionParams: {
      authToken: localStorage.getItem('auth_token'),
    },
  })
);

// Split traffic: subscriptions via WS, queries/mutations via HTTP
const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  from([errorLink, authLink, httpLink])
);

// Subscription definition
const COMMENT_ADDED = gql`
  subscription OnCommentAdded($postId: ID!) {
    commentAdded(postId: $postId) {
      id
      body
      createdAt
      author {
        id
        name
        avatar
      }
    }
  }
`;

// Using subscriptions in a component
import { useSubscription } from '@apollo/client';

function PostComments({ postId }: { postId: string }) {
  const { data: subData } = useSubscription(COMMENT_ADDED, {
    variables: { postId },
    onData: ({ client, data }) => {
      // Update cache when new comment arrives
      const newComment = data.data.commentAdded;
      client.cache.modify({
        id: `Post:${postId}`,
        fields: {
          comments(existing = []) {
            const newRef = client.cache.writeFragment({
              data: newComment,
              fragment: gql`
                fragment NewComment on Comment {
                  id body createdAt
                  author { id name avatar }
                }
              `,
            });
            return [...existing, newRef];
          },
        },
      });
    },
  });

  // Render comments...
}

Error Handling Patterns

Robust error handling is critical for production GraphQL applications. Apollo provides multiple layers of error handling: link-level, query-level, and component-level.

// Comprehensive error handling
import { useQuery, useMutation } from '@apollo/client';

// Custom hook with error categorization
function usePostQuery(slug: string) {
  const { data, loading, error, refetch } = useQuery(GET_POST, {
    variables: { slug },
    errorPolicy: 'all', // Return partial data with errors
    onError: (error) => {
      // Log to error tracking service
      if (error.networkError) {
        trackError('network', error.networkError);
      }
      error.graphQLErrors?.forEach(gqlError => {
        if (gqlError.extensions?.code === 'UNAUTHENTICATED') {
          // Redirect to login
          window.location.href = '/login';
        }
        trackError('graphql', gqlError);
      });
    },
  });

  // Categorize the error for the UI
  const errorInfo = error
    ? {
        isNetwork: !!error.networkError,
        isAuth: error.graphQLErrors?.some(
          e => e.extensions?.code === 'UNAUTHENTICATED'
        ),
        isNotFound: error.graphQLErrors?.some(
          e => e.extensions?.code === 'NOT_FOUND'
        ),
        message: error.message,
      }
    : null;

  return { data, loading, error: errorInfo, refetch };
}

// Retry logic with exponential backoff
import { RetryLink } from '@apollo/client/link/retry';

const retryLink = new RetryLink({
  delay: {
    initial: 300,
    max: 5000,
    jitter: true,
  },
  attempts: {
    max: 3,
    retryIf: (error, _operation) => !!error,
  },
});

Building the GraphQL Server

A complete Apollo Server setup with type definitions, resolvers, authentication, and data sources.

// server.ts — Apollo Server with Express
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import express from 'express';

const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
    avatar: String
    role: Role!
    posts: [Post!]!
  }

  type Post {
    id: ID!
    title: String!
    slug: String!
    excerpt: String
    content: String!
    publishedAt: String
    tags: [String!]!
    author: User!
    comments: [Comment!]!
  }

  type Comment {
    id: ID!
    body: String!
    createdAt: String!
    author: User!
  }

  enum Role { ADMIN AUTHOR READER }

  input PostFilter {
    published: Boolean
    tag: String
    authorId: ID
  }

  input CreatePostInput {
    title: String!
    content: String!
    tags: [String!]
  }

  type Query {
    posts(limit: Int!, offset: Int!, filter: PostFilter): [Post!]!
    postsCount(filter: PostFilter): Int!
    post(slug: String!): Post
    me: User
  }

  type Mutation {
    createPost(input: CreatePostInput!): Post!
    updatePost(id: ID!, input: CreatePostInput!): Post!
    deletePost(id: ID!): Post!
    addComment(postId: ID!, body: String!): Comment!
  }

  type Subscription {
    commentAdded(postId: ID!): Comment!
  }
`;

const resolvers = {
  Query: {
    posts: async (_, { limit, offset, filter }, { dataSources }) => {
      return dataSources.postsAPI.getPosts({ limit, offset, filter });
    },
    post: async (_, { slug }, { dataSources }) => {
      return dataSources.postsAPI.getPostBySlug(slug);
    },
    me: async (_, __, { user }) => user,
  },
  Mutation: {
    createPost: async (_, { input }, { user, dataSources }) => {
      if (!user) throw new Error('Authentication required');
      return dataSources.postsAPI.createPost(input, user.id);
    },
  },
  Post: {
    author: async (post, _, { dataSources }) => {
      return dataSources.usersAPI.getUser(post.authorId);
    },
  },
};

const server = new ApolloServer({ typeDefs, resolvers });
await server.start();

const app = express();
app.use('/graphql', express.json(), expressMiddleware(server, {
  context: async ({ req }) => {
    const token = req.headers.authorization?.replace('Bearer ', '');
    const user = token ? await verifyToken(token) : null;
    return { user, dataSources: createDataSources() };
  },
}));

app.listen(4000);

Apollo Client Best Practices

  • Use fragments to share field selections across queries. This eliminates duplication and ensures consistency when the same entity appears in multiple queries.
  • Set appropriate fetch policies per query. Use cache-first for rarely changing data, cache-and-network for frequently updated data, and network-only when you always need fresh data.
  • Implement optimistic responses for mutations that modify visible data. This makes the UI feel instant by updating the cache immediately while the server request is in flight.
  • Use the errorPolicy option to handle partial errors gracefully. Setting it to "all" returns both data and errors, letting you show available data even when some fields fail.
  • Configure type policies for pagination. Apollo will not automatically merge paginated results; you need to define merge and read functions in your cache type policies.
  • Co-locate queries with components. Keep GraphQL operations in the same file or directory as the component that uses them, making it easy to understand data dependencies.
  • Use Apollo DevTools for debugging. The browser extension shows your cache contents, active queries, and mutation history, making it easier to diagnose data issues.
  • Generate TypeScript types from your GraphQL schema using tools like graphql-codegen. This provides end-to-end type safety from schema to component props.

Try our related developer tools

FAQ

Should I use GraphQL or REST for my project?

Use GraphQL when you have complex, interconnected data that multiple clients (web, mobile) consume differently, or when over-fetching and under-fetching are real performance problems. Use REST when you have simple CRUD operations, need HTTP caching, or your team is already productive with REST. Many teams use both: GraphQL for frontend-facing APIs and REST for internal microservice communication.

Does Apollo Client replace Redux for state management?

Apollo Client replaces Redux for server state (data from APIs). Apollo cache acts as a single source of truth for all server data, eliminating the need for Redux actions and reducers to manage API responses. For client state (UI toggles, form data, theme preferences), you can use Apollo reactive variables, React context, or a lightweight library like Zustand.

How do I handle authentication with Apollo?

Use Apollo Link to attach authentication headers to every request. The setContext link lets you read the auth token from storage and add it to request headers. For token refresh, use an error link that catches 401 responses, refreshes the token, and retries the failed request automatically.

Is GraphQL slower than REST?

GraphQL query parsing and validation add a small overhead compared to REST. However, GraphQL often improves overall performance by reducing the number of network requests (one query instead of multiple REST calls) and eliminating over-fetching. For most applications, the network savings outweigh the parsing overhead. Use persisted queries and response caching for optimal performance.

What alternatives to Apollo Client exist?

The main alternatives are urql (lighter weight, plugin-based), Relay (Facebook, opinionated, compiler-driven), and TanStack Query with graphql-request (familiar API if you already use TanStack Query for REST). Apollo Client has the largest ecosystem, best documentation, and most features, but urql is a strong choice for simpler applications that do not need Apollo full feature set.

𝕏 Twitterin LinkedIn
Czy to było pomocne?

Bądź na bieżąco

Otrzymuj cotygodniowe porady i nowe narzędzia.

Bez spamu. Zrezygnuj kiedy chcesz.

Try These Related Tools

GQLJSON to GraphQL

Related Articles

GraphQL vs REST API: Kiedy uzywac ktorego w 2026?

Doglebne porownanie GraphQL i REST API z przykladami kodu. Roznice architektoniczne, wzorce pobierania danych, cache i kryteria wyboru.

Kompletny przewodnik po React Hooks: useState, useEffect i Custom Hooks

Opanuj React Hooks z praktycznymi przykladami. useState, useEffect, useContext, useReducer, useMemo, useCallback, custom hooks i React 18+ concurrent hooks.