GraphQL

Apollo Client Advanced

20 min Lesson 20 of 35

Advanced Apollo Client Techniques

Beyond basic queries, Apollo Client offers powerful features for mutations, cache management, optimistic UI, lazy queries, subscriptions, and local state. Master these techniques to build highly responsive and efficient GraphQL applications.

useMutation Hook

The useMutation hook executes GraphQL mutations to create, update, or delete data.

Basic useMutation Example:
import React, { useState } from 'react';
import { useMutation, gql } from '@apollo/client';

const CREATE_POST = gql`
  mutation CreatePost($title: String!, $content: String!) {
    createPost(title: $title, content: $content) {
      id
      title
      content
      createdAt
    }
  }
`;

function CreatePostForm() {
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');

  const [createPost, { data, loading, error }] = useMutation(CREATE_POST);

  const handleSubmit = async (e) => {
    e.preventDefault();

    try {
      await createPost({
        variables: { title, content }
      });

      // Clear form on success
      setTitle('');
      setContent('');
      alert('Post created successfully!');
    } catch (err) {
      console.error('Mutation error:', err);
    }
  };

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

export default CreatePostForm;

Cache Updates After Mutations

After a mutation, you need to update the Apollo cache so the UI reflects the changes without refetching.

Manual Cache Update:
import { useMutation, gql } from '@apollo/client';

const CREATE_POST = gql`
  mutation CreatePost($title: String!, $content: String!) {
    createPost(title: $title, content: $content) {
      id
      title
      content
    }
  }
`;

const GET_POSTS = gql`
  query GetPosts {
    posts {
      id
      title
      content
    }
  }
`;

function CreatePostForm() {
  const [createPost] = useMutation(CREATE_POST, {
    update(cache, { data: { createPost } }) {
      // Read existing posts from cache
      const existingPosts = cache.readQuery({ query: GET_POSTS });

      // Write updated posts back to cache
      cache.writeQuery({
        query: GET_POSTS,
        data: {
          posts: [...existingPosts.posts, createPost]
        }
      });
    }
  });

  // Form implementation...
}
Refetch Queries After Mutation:
const [deletePost] = useMutation(DELETE_POST, {
  refetchQueries: [
    { query: GET_POSTS },
    { query: GET_USER_POSTS, variables: { userId: user.id } }
  ]
});

// Or use refetchQueries callback
const [updatePost] = useMutation(UPDATE_POST, {
  refetchQueries: (result) => [
    { query: GET_POST, variables: { id: result.data.updatePost.id } }
  ]
});

Optimistic UI

Optimistic UI updates the cache immediately (before server response) to make the app feel instant. If the mutation fails, Apollo reverts the change.

Optimistic Response Example:
import { useMutation, gql } from '@apollo/client';

const LIKE_POST = gql`
  mutation LikePost($postId: ID!) {
    likePost(postId: $postId) {
      id
      likes
      isLiked
    }
  }
`;

function LikeButton({ post }) {
  const [likePost] = useMutation(LIKE_POST, {
    optimisticResponse: {
      likePost: {
        __typename: 'Post',
        id: post.id,
        likes: post.likes + 1,
        isLiked: true
      }
    }
  });

  return (
    <button onClick={() => likePost({ variables: { postId: post.id } })}>
      {post.isLiked ? 'Unlike' : 'Like'} ({post.likes})
    </button>
  );
}
Optimistic UI Best Practices:
  • Use for fast, low-risk operations (likes, follows, toggles)
  • Keep optimistic response structure identical to actual response
  • Include __typename in optimistic response for cache normalization
  • Avoid for critical operations (payments, data deletion)

useLazyQuery Hook

Unlike useQuery which executes immediately, useLazyQuery returns a function you can call manually when needed.

Lazy Query Example:
import React, { useState } from 'react';
import { useLazyQuery, gql } from '@apollo/client';

const SEARCH_USERS = gql`
  query SearchUsers($query: String!) {
    searchUsers(query: $query) {
      id
      name
      email
      avatar
    }
  }
`;

function UserSearch() {
  const [query, setQuery] = useState('');

  // Returns [executeQuery, { data, loading, error }]
  const [searchUsers, { data, loading, error }] = useLazyQuery(SEARCH_USERS);

  const handleSearch = (e) => {
    e.preventDefault();
    searchUsers({ variables: { query } });
  };

  return (
    <div>
      <form onSubmit={handleSearch}>
        <input
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Search users..."
        />
        <button type="submit">Search</button>
      </form>

      {loading && <p>Searching...</p>}
      {error && <p>Error: {error.message}</p>}

      {data && (
        <ul>
          {data.searchUsers.map(user => (
            <li key={user.id}>
              <img src={user.avatar} alt={user.name} />
              {user.name} - {user.email}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default UserSearch;

useSubscription Hook

The useSubscription hook subscribes to real-time updates via WebSocket.

Subscription Example:
import React from 'react';
import { useSubscription, gql } from '@apollo/client';

const MESSAGE_SUBSCRIPTION = gql`
  subscription OnMessageAdded($channelId: ID!) {
    messageAdded(channelId: $channelId) {
      id
      content
      author {
        id
        name
        avatar
      }
      createdAt
    }
  }
`;

function ChatMessages({ channelId, messages }) {
  const { data, loading } = useSubscription(MESSAGE_SUBSCRIPTION, {
    variables: { channelId },
    onData: ({ client, data }) => {
      // Handle new message
      console.log('New message:', data.data.messageAdded);
    }
  });

  return (
    <div>
      {messages.map(msg => (
        <div key={msg.id}>
          <strong>{msg.author.name}:</strong> {msg.content}
        </div>
      ))}
      {loading && <p>Connecting to chat...</p>}
    </div>
  );
}

export default ChatMessages;

Local State Management

Apollo Client can manage local state alongside remote data, eliminating the need for separate state management libraries.

Local State with Reactive Variables:
import { makeVar, useReactiveVar } from '@apollo/client';

// Create reactive variable
export const isLoggedInVar = makeVar(false);
export const currentUserVar = makeVar(null);
export const themeVar = makeVar('light');

// Use in components
function Header() {
  const isLoggedIn = useReactiveVar(isLoggedInVar);
  const currentUser = useReactiveVar(currentUserVar);
  const theme = useReactiveVar(themeVar);

  const toggleTheme = () => {
    themeVar(theme === 'light' ? 'dark' : 'light');
  };

  return (
    <header className={theme}>
      {isLoggedIn ? (
        <div>Welcome, {currentUser.name}</div>
      ) : (
        <button>Log In</button>
      )}
      <button onClick={toggleTheme}>
        Toggle Theme
      </button>
    </header>
  );
}
Reading and Writing Reactive Variables:
import { isLoggedInVar, currentUserVar } from './cache';

// Read current value
const isLoggedIn = isLoggedInVar();

// Update value
isLoggedInVar(true);
currentUserVar({ id: '1', name: 'John Doe', email: 'john@example.com' });

// In mutations
const [login] = useMutation(LOGIN, {
  onCompleted: (data) => {
    isLoggedInVar(true);
    currentUserVar(data.login.user);
  }
});

const [logout] = useMutation(LOGOUT, {
  onCompleted: () => {
    isLoggedInVar(false);
    currentUserVar(null);
  }
});

Cache Manipulation

Directly read, write, and modify the Apollo cache for advanced use cases.

Direct Cache Access:
import { useApolloClient } from '@apollo/client';

function UserProfile({ userId }) {
  const client = useApolloClient();

  // Read from cache
  const cachedUser = client.readFragment({
    id: `User:${userId}`,
    fragment: gql`
      fragment UserData on User {
        id
        name
        email
      }
    `
  });

  // Write to cache
  const updateUserInCache = (updates) => {
    client.writeFragment({
      id: `User:${userId}`,
      fragment: gql`
        fragment UserData on User {
          id
          name
          email
        }
      `,
      data: {
        ...cachedUser,
        ...updates
      }
    });
  };

  // Evict from cache (remove)
  const removeUserFromCache = () => {
    client.cache.evict({ id: `User:${userId}` });
    client.cache.gc(); // Garbage collect
  };

  return (
    <div>
      {/* Component UI */}
    </div>
  );
}

Error Handling in Mutations

Comprehensive Mutation Error Handling:
const [updateUser, { loading, error }] = useMutation(UPDATE_USER, {
  onError: (error) => {
    if (error.graphQLErrors) {
      error.graphQLErrors.forEach(({ message, extensions }) => {
        if (extensions?.code === 'UNAUTHENTICATED') {
          // Redirect to login
          window.location.href = '/login';
        } else if (extensions?.code === 'BAD_USER_INPUT') {
          // Show validation errors
          alert(`Validation error: ${message}`);
        }
      });
    }

    if (error.networkError) {
      alert('Network error. Please try again.');
    }
  },
  onCompleted: (data) => {
    alert('User updated successfully!');
  }
});
Warning: When using optimistic UI with cache updates, ensure your optimistic response structure matches the actual server response exactly. Mismatches can cause cache inconsistencies and UI bugs.
Exercise:
  1. Create a mutation to add a comment to a post with useMutation
  2. Implement manual cache update to add the new comment to the post's comment list
  3. Add optimistic UI to instantly show the comment before server confirmation
  4. Build a search feature using useLazyQuery that only executes when the user submits a form
  5. Create reactive variables for dark mode preference and use them across multiple components