We use cookies to enhance your experience on the site
CodeWorlds

Apollo Client - Comprehensive GraphQL Integration with React

Apollo Client is a powerful library for state management and communication with GraphQL servers in React applications. It offers advanced caching features, optimistic updates, subscription support, and much more.

Installation and Basic Configuration

Installation

1npm install @apollo/client graphql
2# Or with additional tooling
3npm install @apollo/client graphql apollo-link-error apollo-upload-client

Basic Apollo Client Configuration

1// apolloClient.js
2import { ApolloClient, InMemoryCache, createHttpLink, from } from '@apollo/client';
3import { setContext } from '@apollo/client/link/context';
4import { onError } from '@apollo/client/link/error';
5
6// HTTP link for GraphQL server communication
7const httpLink = createHttpLink({
8  uri: process.env.REACT_APP_GRAPHQL_ENDPOINT || 'http://localhost:4000/graphql',
9  credentials: 'include' // For cookies/session
10});
11
12// Link for authorization handling
13const authLink = setContext((_, { headers }) => {
14  const token = localStorage.getItem('authToken');
15
16  return {
17    headers: {
18      ...headers,
19      authorization: token ? `Bearer ${token}` : '',
20      'x-api-version': '1.0',
21      'x-client-name': 'react-app'
22    }
23  };
24});
25
26// Link for error handling
27const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
28  if (graphQLErrors) {
29    graphQLErrors.forEach(({ message, locations, path, extensions }) => {
30      console.error(
31        `GraphQL error: Message: ${message}, Location: ${locations}, Path: ${path}`
32      );
33
34      // Handle specific errors
35      if (extensions?.code === 'UNAUTHENTICATED') {
36        localStorage.removeItem('authToken');
37        window.location.href = '/login';
38      }
39    });
40  }
41
42  if (networkError) {
43    console.error(`Network error: ${networkError}`);
44
45    // Retry logic for network errors
46    if (networkError.statusCode === 500) {
47      return forward(operation);
48    }
49  }
50});
51
52// Cache configuration
53const cache = new InMemoryCache({
54  typePolicies: {
55    User: {
56      fields: {
57        // Cache configuration for user fields
58        posts: {
59          merge(existing = [], incoming) {
60            return [...existing, ...incoming];
61          }
62        }
63      }
64    },
65    Post: {
66      fields: {
67        comments: {
68          merge(existing = [], incoming) {
69            return incoming;
70          }
71        }
72      }
73    }
74  }
75});
76
77// Main Apollo client
78const apolloClient = new ApolloClient({
79  link: from([errorLink, authLink, httpLink]),
80  cache,
81  defaultOptions: {
82    watchQuery: {
83      errorPolicy: 'all',
84      notifyOnNetworkStatusChange: true
85    },
86    query: {
87      errorPolicy: 'all'
88    }
89  },
90  connectToDevTools: process.env.NODE_ENV === 'development'
91});
92
93export default apolloClient;

Integration with React Application

1// App.js
2import React from 'react';
3import { ApolloProvider } from '@apollo/client';
4import apolloClient from './apolloClient';
5import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
6import Dashboard from './components/Dashboard';
7import UserProfile from './components/UserProfile';
8
9function App() {
10  return (
11    <ApolloProvider client={apolloClient}>
12      <Router>
13        <div className="App">
14          <Routes>
15            <Route path="/" element={<Dashboard />} />
16            <Route path="/profile/:id" element={<UserProfile />} />
17          </Routes>
18        </div>
19      </Router>
20    </ApolloProvider>
21  );
22}
23
24export default App;

Executing Queries

Basic Queries with useQuery

1// GraphQL query definition
2import { gql, useQuery } from '@apollo/client';
3
4const GET_USERS = gql`
5  query GetUsers($limit: Int, $offset: Int) {
6    users(limit: $limit, offset: $offset) {
7      id
8      name
9      email
10      avatar
11      posts {
12        id
13        title
14        createdAt
15      }
16    }
17  }
18`;
19
20function UsersList() {
21  const { data, loading, error, refetch, fetchMore } = useQuery(GET_USERS, {
22    variables: { limit: 10, offset: 0 },
23    notifyOnNetworkStatusChange: true,
24    errorPolicy: 'all'
25  });
26
27  if (loading) return <div className="loading">Loading users...</div>;
28  if (error) return <div className="error">Error: {error.message}</div>;
29
30  const loadMoreUsers = () => {
31    fetchMore({
32      variables: {
33        offset: data.users.length
34      },
35      updateQuery: (prev, { fetchMoreResult }) => {
36        if (!fetchMoreResult) return prev;
37
38        return {
39          ...prev,
40          users: [...prev.users, ...fetchMoreResult.users]
41        };
42      }
43    });
44  };
45
46  return (
47    <div>
48      <h2>Users List</h2>
49      <button onClick={() => refetch()}>Refresh</button>
50
51      <div className="users-grid">
52        {data.users.map(user => (
53          <div key={user.id} className="user-card">
54            <img src={user.avatar} alt={user.name} />
55            <h3>{user.name}</h3>
56            <p>{user.email}</p>
57            <p>Posts: {user.posts.length}</p>
58          </div>
59        ))}
60      </div>
61
62      <button onClick={loadMoreUsers}>Load More</button>
63    </div>
64  );
65}

Lazy Queries - On-Demand Queries

1import { gql, useLazyQuery } from '@apollo/client';
2import { useState } from 'react';
3
4const SEARCH_USERS = gql`
5  query SearchUsers($query: String!) {
6    searchUsers(query: $query) {
7      id
8      name
9      email
10      avatar
11    }
12  }
13`;
14
15function UserSearch() {
16  const [searchTerm, setSearchTerm] = useState('');
17  const [searchUsers, { data, loading, error }] = useLazyQuery(SEARCH_USERS);
18
19  const handleSearch = (e) => {
20    e.preventDefault();
21    if (searchTerm.trim()) {
22      searchUsers({
23        variables: { query: searchTerm }
24      });
25    }
26  };
27
28  return (
29    <div>
30      <form onSubmit={handleSearch}>
31        <input
32          type="text"
33          value={searchTerm}
34          onChange={(e) => setSearchTerm(e.target.value)}
35          />
36        <button type="submit">Search</button>
37      </form>
38
39      {loading && <div>Searching...</div>}
40      {error && <div>Search error: {error.message}</div>}
41
42      {data?.searchUsers && (
43        <div>
44          <h3>Search Results:</h3>
45          {data.searchUsers.map(user => (
46            <div key={user.id}>{user.name} - {user.email}</div>
47          ))}
48        </div>
49      )}
50    </div>
51  );
52}

Mutations

Basic Mutations with useMutation

1import { gql, useMutation } from '@apollo/client';
2import { useState } from 'react';
3
4const CREATE_POST = gql`
5  mutation CreatePost($input: CreatePostInput!) {
6    createPost(input: $input) {
7      id
8      title
9      content
10      author {
11        id
12        name
13      }
14      createdAt
15    }
16  }
17`;
18
19const GET_POSTS = gql`
20  query GetPosts {
21    posts {
22      id
23      title
24      content
25      author {
26        id
27        name
28      }
29      createdAt
30    }
31  }
32`;
33
34function CreatePostForm() {
35  const [title, setTitle] = useState('');
36  const [content, setContent] = useState('');
37
38  const [createPost, { loading, error }] = useMutation(CREATE_POST, {
39    // Update cache after creating a post
40    update(cache, { data: { createPost } }) {
41      const existingPosts = cache.readQuery({ query: GET_POSTS });
42
43      cache.writeQuery({
44        query: GET_POSTS,
45        data: {
46          posts: [createPost, ...existingPosts.posts]
47        }
48      });
49    },
50
51    // Optimistic update
52    optimisticResponse: {
53      createPost: {
54        __typename: 'Post',
55        id: 'temp-id',
56        title,
57        content,
58        author: {
59          __typename: 'User',
60          id: 'current-user-id',
61          name: 'You'
62        },
63        createdAt: new Date().toISOString()
64      }
65    },
66
67    // Error handling
68    onError: (error) => {
69      console.error('Error creating post:', error);
70    },
71
72    onCompleted: (data) => {
73      console.log('Post created:', data.createPost);
74      setTitle('');
75      setContent('');
76    }
77  });
78
79  const handleSubmit = async (e) => {
80    e.preventDefault();
81
82    if (!title.trim() || !content.trim()) {
83      alert('Please fill in all fields');
84      return;
85    }
86
87    try {
88      await createPost({
89        variables: {
90          input: { title, content }
91        }
92      });
93    } catch (err) {
94      console.error('Error creating post:', err);
95    }
96  };
97
98  return (
99    <form onSubmit={handleSubmit}>
100      <div>
101        <label>Title:</label>
102        <input
103          type="text"
104          value={title}
105          onChange={(e) => setTitle(e.target.value)}
106          disabled={loading}
107        />
108      </div>
109
110      <div>
111        <label>Content:</label>
112        <textarea
113          value={content}
114          onChange={(e) => setContent(e.target.value)}
115          disabled={loading}
116        />
117      </div>
118
119      <button type="submit" disabled={loading}>
120        {loading ? 'Creating...' : 'Create Post'}
121      </button>
122
123      {error && <div className="error">Error: {error.message}</div>}
124    </form>
125  );
126}

Advanced Cache Management

1import { gql, useMutation, useQuery } from '@apollo/client';
2
3const DELETE_POST = gql`
4  mutation DeletePost($id: ID!) {
5    deletePost(id: $id) {
6      id
7    }
8  }
9`;
10
11const UPDATE_POST = gql`
12  mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
13    updatePost(id: $id, input: $input) {
14      id
15      title
16      content
17      updatedAt
18    }
19  }
20`;
21
22function PostManager({ postId }) {
23  const [deletePost] = useMutation(DELETE_POST, {
24    // Remove from cache after deletion
25    update(cache, { data: { deletePost } }) {
26      cache.modify({
27        fields: {
28          posts(existingPosts = [], { readField }) {
29            return existingPosts.filter(
30              postRef => deletePost.id !== readField('id', postRef)
31            );
32          }
33        }
34      });
35
36      // Also remove the individual post from cache
37      cache.evict({ id: cache.identify({ __typename: 'Post', id: deletePost.id }) });
38      cache.gc();
39    }
40  });
41
42  const [updatePost] = useMutation(UPDATE_POST, {
43    // No update needed - Apollo automatically updates cache
44    // if returned data has the same ID and __typename
45  });
46
47  const handleDelete = async () => {
48    if (window.confirm('Are you sure you want to delete this post?')) {
49      try {
50        await deletePost({
51          variables: { id: postId }
52        });
53      } catch (error) {
54        console.error('Error deleting post:', error);
55      }
56    }
57  };
58
59  const handleUpdate = async (newTitle, newContent) => {
60    try {
61      await updatePost({
62        variables: {
63          id: postId,
64          input: { title: newTitle, content: newContent }
65        }
66      });
67    } catch (error) {
68      console.error('Error updating post:', error);
69    }
70  };
71
72  return (
73    <div>
74      <button onClick={handleDelete}>Delete Post</button>
75      {/* Edit form */}
76    </div>
77  );
78}

Subscriptions

WebSocket Configuration for Subscriptions

1// apolloClient.js - extended configuration
2import { split } from '@apollo/client';
3import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
4import { createClient } from 'graphql-ws';
5import { getMainDefinition } from '@apollo/client/utilities';
6
7// WebSocket link for subscriptions
8const wsLink = new GraphQLWsLink(
9  createClient({
10    url: process.env.REACT_APP_GRAPHQL_WS_ENDPOINT || 'ws://localhost:4000/graphql',
11    connectionParams: () => {
12      const token = localStorage.getItem('authToken');
13      return {
14        authorization: token ? `Bearer ${token}` : ''
15      };
16    },
17    on: {
18      connected: () => console.log('WebSocket connected'),
19      closed: () => console.log('WebSocket disconnected')
20    }
21  })
22);
23
24// Traffic splitting - HTTP for queries/mutations, WebSocket for subscriptions
25const splitLink = split(
26  ({ query }) => {
27    const definition = getMainDefinition(query);
28    return (
29      definition.kind === 'OperationDefinition' &&
30      definition.operation === 'subscription'
31    );
32  },
33  wsLink,
34  from([errorLink, authLink, httpLink])
35);
36
37// Updated client
38const apolloClient = new ApolloClient({
39  link: splitLink, // Use splitLink instead of from([...])
40  cache,
41  // ... rest of configuration
42});

Using Subscriptions in Components

1import { gql, useSubscription, useQuery } from '@apollo/client';
2
3const POST_ADDED_SUBSCRIPTION = gql`
4  subscription PostAdded {
5    postAdded {
6      id
7      title
8      content
9      author {
10        id
11        name
12      }
13      createdAt
14    }
15  }
16`;
17
18const COMMENT_ADDED_SUBSCRIPTION = gql`
19  subscription CommentAdded($postId: ID!) {
20    commentAdded(postId: $postId) {
21      id
22      content
23      author {
24        id
25        name
26      }
27      createdAt
28    }
29  }
30`;
31
32function LivePostsFeed() {
33  const { data: postsData, loading } = useQuery(GET_POSTS);
34
35  // Subscribe to new posts
36  useSubscription(POST_ADDED_SUBSCRIPTION, {
37    onSubscriptionData: ({ subscriptionData, client }) => {
38      const newPost = subscriptionData.data.postAdded;
39
40      // Update cache
41      const existingPosts = client.readQuery({ query: GET_POSTS });
42      client.writeQuery({
43        query: GET_POSTS,
44        data: {
45          posts: [newPost, ...existingPosts.posts]
46        }
47      });
48
49      // Show notification
50      showNotification(`New post: ${newPost.title}`);
51    }
52  });
53
54  if (loading) return <div>Loading...</div>;
55
56  return (
57    <div>
58      <h2>Live Posts</h2>
59      {postsData?.posts.map(post => (
60        <LivePostCard key={post.id} post={post} />
61      ))}
62    </div>
63  );
64}
65
66function LivePostCard({ post }) {
67  // Subscribe to new comments for this post
68  useSubscription(COMMENT_ADDED_SUBSCRIPTION, {
69    variables: { postId: post.id },
70    onSubscriptionData: ({ subscriptionData }) => {
71      const newComment = subscriptionData.data.commentAdded;
72      console.log(`New comment on post ${post.title}:`, newComment.content);
73    }
74  });
75
76  return (
77    <div className="post-card">
78      <h3>{post.title}</h3>
79      <p>{post.content}</p>
80      <small>Author: {post.author.name}</small>
81    </div>
82  );
83}

Advanced Patterns and Optimizations

1. Fragment Composition

1// fragments.js
2import { gql } from '@apollo/client';
3
4export const USER_FRAGMENT = gql`
5  fragment UserInfo on User {
6    id
7    name
8    email
9    avatar
10  }
11`;
12
13export const POST_FRAGMENT = gql`
14  fragment PostInfo on Post {
15    id
16    title
17    content
18    createdAt
19    author {
20      ...UserInfo
21    }
22  }
23  ${USER_FRAGMENT}
24`;
25
26// Using fragments
27const GET_POSTS_WITH_FRAGMENTS = gql`
28  query GetPosts {
29    posts {
30      ...PostInfo
31    }
32  }
33  ${POST_FRAGMENT}
34`;

2. Custom Hooks for GraphQL

1// hooks/usePostOperations.js
2import { gql, useQuery, useMutation } from '@apollo/client';
3import { POST_FRAGMENT } from '../fragments';
4
5const GET_POSTS = gql`
6  query GetPosts($limit: Int, $offset: Int) {
7    posts(limit: $limit, offset: $offset) {
8      ...PostInfo
9    }
10  }
11  ${POST_FRAGMENT}
12`;
13
14const CREATE_POST = gql`
15  mutation CreatePost($input: CreatePostInput!) {
16    createPost(input: $input) {
17      ...PostInfo
18    }
19  }
20  ${POST_FRAGMENT}
21`;
22
23export function usePostOperations() {
24  const { data, loading, error, refetch, fetchMore } = useQuery(GET_POSTS, {
25    variables: { limit: 10, offset: 0 }
26  });
27
28  const [createPostMutation, { loading: creating }] = useMutation(CREATE_POST, {
29    update(cache, { data: { createPost } }) {
30      const existingPosts = cache.readQuery({ query: GET_POSTS });
31      cache.writeQuery({
32        query: GET_POSTS,
33        data: {
34          posts: [createPost, ...existingPosts.posts]
35        }
36      });
37    }
38  });
39
40  const createPost = async (postData) => {
41    try {
42      const result = await createPostMutation({
43        variables: { input: postData }
44      });
45      return { success: true, post: result.data.createPost };
46    } catch (error) {
47      return { success: false, error: error.message };
48    }
49  };
50
51  const loadMore = () => {
52    return fetchMore({
53      variables: { offset: data?.posts?.length || 0 }
54    });
55  };
56
57  return {
58    posts: data?.posts || [],
59    loading,
60    error,
61    creating,
62    createPost,
63    refetch,
64    loadMore
65  };
66}
67
68// Using the custom hook
69function PostsPage() {
70  const { posts, loading, createPost, loadMore } = usePostOperations();
71
72  const handleCreatePost = async (formData) => {
73    const result = await createPost(formData);
74    if (result.success) {
75      console.log('Post created successfully');
76    } else {
77      console.error('Failed to create post:', result.error);
78    }
79  };
80
81  return (
82    <div>
83      <CreatePostForm onSubmit={handleCreatePost} />
84      {loading ? (
85        <div>Loading...</div>
86      ) : (
87        <PostsList posts={posts} onLoadMore={loadMore} />
88      )}
89    </div>
90  );
91}

3. Error Boundaries for GraphQL

1import React from 'react';
2import { ApolloError } from '@apollo/client';
3
4class GraphQLErrorBoundary extends React.Component {
5  constructor(props) {
6    super(props);
7    this.state = { hasError: false, error: null };
8  }
9
10  static getDerivedStateFromError(error) {
11    return { hasError: true, error };
12  }
13
14  componentDidCatch(error, errorInfo) {
15    if (error instanceof ApolloError) {
16      console.error('GraphQL Error:', error.graphQLErrors);
17      console.error('Network Error:', error.networkError);
18    }
19
20    // Send error to monitoring system
21    this.logErrorToService(error, errorInfo);
22  }
23
24  logErrorToService(error, errorInfo) {
25    // Integration with Sentry, LogRocket, etc.
26    console.error('Error logged:', error, errorInfo);
27  }
28
29  render() {
30    if (this.state.hasError) {
31      return (
32        <div className="error-boundary">
33          <h2>Something went wrong with GraphQL</h2>
34          <details>
35            <summary>Error details</summary>
36            <pre>{this.state.error?.message}</pre>
37          </details>
38          <button onClick={() => this.setState({ hasError: false, error: null })}>
39            Try again
40          </button>
41        </div>
42      );
43    }
44
45    return this.props.children;
46  }
47}
48
49// Usage
50function App() {
51  return (
52    <ApolloProvider client={apolloClient}>
53      <GraphQLErrorBoundary>
54        <Router>
55          {/* Your application */}
56        </Router>
57      </GraphQLErrorBoundary>
58    </ApolloProvider>
59  );
60}

4. Testing with Apollo Client

1// __tests__/UsersList.test.js
2import React from 'react';
3import { render, screen, waitFor } from '@testing-library/react';
4import { MockedProvider } from '@apollo/client/testing';
5import UsersList, { GET_USERS } from '../UsersList';
6
7const mocks = [
8  {
9    request: {
10      query: GET_USERS,
11      variables: { limit: 10, offset: 0 }
12    },
13    result: {
14      data: {
15        users: [
16          {
17            id: '1',
18            name: 'John Doe',
19            email: 'john@example.com',
20            avatar: 'https://example.com/avatar1.jpg',
21            posts: []
22          }
23        ]
24      }
25    }
26  }
27];
28
29test('renders users list', async () => {
30  render(
31    <MockedProvider mocks={mocks} addTypename={false}>
32      <UsersList />
33    </MockedProvider>
34  );
35
36  // Check loading state
37  expect(screen.getByText('Loading users...')).toBeInTheDocument();
38
39  // Wait for data to load
40  await waitFor(() => {
41    expect(screen.getByText('John Doe')).toBeInTheDocument();
42  });
43});
44
45test('handles error state', async () => {
46  const errorMocks = [
47    {
48      request: {
49        query: GET_USERS,
50        variables: { limit: 10, offset: 0 }
51      },
52      error: new Error('Network error')
53    }
54  ];
55
56  render(
57    <MockedProvider mocks={errorMocks} addTypename={false}>
58      <UsersList />
59    </MockedProvider>
60  );
61
62  await waitFor(() => {
63    expect(screen.getByText(/Error: Network error/)).toBeInTheDocument();
64  });
65});

When to Use Apollo Client?

Apollo Client is the best choice when:

  • Your backend uses GraphQL -- Apollo is optimized for this protocol
  • You need advanced caching -- Apollo InMemoryCache automatically normalizes and deduplicates data
  • Your application requires optimistic updates -- e.g., "liking" a post shows the change instantly before the server confirms
  • You work with real-time data -- GraphQL subscriptions enable real-time communication
  • You want DevTools -- Apollo DevTools in the browser let you inspect cache, queries, and mutations

Alternatives to Apollo Client:

  • urql -- lighter GraphQL library with a simpler API
  • React Query + graphql-request -- React Query for caching + simple GraphQL client
  • Relay -- Meta's solution, optimized for large applications

Apollo Client is a comprehensive solution for managing GraphQL state in React applications. It offers powerful caching, optimistic updates, and real-time subscriptions that enable building performant and responsive applications -- like an advanced communication system on a spaceship that not only receives signals but also intelligently buffers and processes them.

Go to CodeWorlds