React Apollo Client
Introduction
Apollo Client is a comprehensive state management library that enables you to manage both local and remote data with GraphQL in React applications. It allows you to fetch, cache, and modify application data, all while automatically updating your UI. Apollo Client helps you structure code in a predictable and declarative way consistent with modern React practices.
In this tutorial, we'll explore how to integrate Apollo Client with your React applications to efficiently communicate with GraphQL APIs.
What is Apollo Client?
Apollo Client is a powerful JavaScript library that implements GraphQL for client applications. It offers several key features:
- Declarative data fetching: Write a GraphQL query and Apollo Client handles requesting and caching data
- Excellent developer experience: Helpful tooling and intelligent caching
- Designed for modern React: Integrates seamlessly with React using hooks or components
- Incrementally adoptable: Can be added to any JavaScript app with minimal configuration
- Universally compatible: Works with any build setup, any GraphQL API, and any GraphQL schema
Setting Up Apollo Client
Let's start by installing the necessary packages:
npm install @apollo/client graphql
Now, let's set up Apollo Client in your React application:
import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
import App from './App';
// Initialize Apollo Client
const client = new ApolloClient({
uri: 'https://your-graphql-endpoint.com/graphql',
cache: new InMemoryCache()
});
// Wrap your application with ApolloProvider
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root')
);
In this setup:
uri
specifies the URL of your GraphQL servercache
is an instance of InMemoryCache, which Apollo Client uses to cache query resultsApolloProvider
is a component that places the Apollo Client on the React context, making it available throughout your component tree
Querying Data with Apollo Client
Apollo Client provides a useQuery
hook that is the primary API for executing queries in a React application.
Basic Query Example
Let's fetch a list of books from a GraphQL API:
import { useQuery, gql } from '@apollo/client';
// Define your GraphQL query
const GET_BOOKS = gql`
query GetBooks {
books {
id
title
author {
name
}
}
}
`;
function BookList() {
const { loading, error, data } = useQuery(GET_BOOKS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h2>Book List</h2>
<ul>
{data.books.map(book => (
<li key={book.id}>
{book.title} by {book.author.name}
</li>
))}
</ul>
</div>
);
}
The useQuery
hook returns an object with:
loading
: boolean indicating if the query is in flighterror
: contains any error encountered during the querydata
: contains the result of your query when it completes
Query with Variables
Often, you'll need to pass variables to your GraphQL queries. Here's how to do that:
import { useQuery, gql } from '@apollo/client';
const GET_BOOK_BY_ID = gql`
query GetBook($id: ID!) {
book(id: $id) {
id
title
description
author {
name
}
}
}
`;
function BookDetails({ bookId }) {
const { loading, error, data } = useQuery(GET_BOOK_BY_ID, {
variables: { id: bookId },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!data || !data.book) return <p>No book found</p>;
const { book } = data;
return (
<div>
<h2>{book.title}</h2>
<p><strong>Author:</strong> {book.author.name}</p>
<p>{book.description}</p>
</div>
);
}
Mutations with Apollo Client
Mutations allow you to modify data on the server and update your local cache. Apollo Client provides a useMutation
hook for this purpose.
import { useMutation, gql } from '@apollo/client';
const ADD_BOOK = gql`
mutation AddBook($title: String!, $authorId: ID!) {
addBook(title: $title, authorId: $authorId) {
id
title
author {
name
}
}
}
`;
function AddBookForm() {
const [title, setTitle] = useState('');
const [authorId, setAuthorId] = useState('');
const [addBook, { data, loading, error }] = useMutation(ADD_BOOK);
const handleSubmit = (e) => {
e.preventDefault();
addBook({ variables: { title, authorId } });
// Reset form
setTitle('');
setAuthorId('');
};
if (loading) return <p>Submitting...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h3>Add New Book</h3>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="title">Title:</label>
<input
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="authorId">Author ID:</label>
<input
id="authorId"
value={authorId}
onChange={(e) => setAuthorId(e.target.value)}
required
/>
</div>
<button type="submit">Add Book</button>
</form>
{data && data.addBook && (
<p>Added book: {data.addBook.title}</p>
)}
</div>
);
}
Updating the Cache After Mutation
After performing a mutation, you'll often want to update the Apollo Client cache to reflect the changes without refetching data from the server:
const [addBook] = useMutation(ADD_BOOK, {
update(cache, { data: { addBook } }) {
// Read the data from the cache for the GET_BOOKS query
const { books } = cache.readQuery({ query: GET_BOOKS });
// Update the cache with our new book
cache.writeQuery({
query: GET_BOOKS,
data: { books: [...books, addBook] },
});
}
});
Optimistic UI
Apollo Client supports optimistic UI updates, which means you can update the UI before receiving a server response:
const [deleteBook] = useMutation(DELETE_BOOK, {
optimisticResponse: {
deleteBook: {
id: bookId,
__typename: "Book",
}
},
update(cache, { data: { deleteBook } }) {
const { books } = cache.readQuery({ query: GET_BOOKS });
cache.writeQuery({
query: GET_BOOKS,
data: { books: books.filter(book => book.id !== deleteBook.id) },
});
}
});
Error Handling
Apollo Client provides comprehensive error handling for both network and GraphQL errors:
function BookList() {
const { loading, error, data } = useQuery(GET_BOOKS);
if (loading) return <p>Loading...</p>;
if (error) {
if (error.networkError) {
return <p>Network Error: Check your connection</p>;
}
if (error.graphQLErrors) {
return (
<div>
<p>GraphQL Errors:</p>
<ul>
{error.graphQLErrors.map(({ message }, i) => (
<li key={i}>{message}</li>
))}
</ul>
</div>
);
}
return <p>Error: {error.message}</p>;
}
// Render data...
}
Apollo Client Caching
One of Apollo Client's most powerful features is its normalized cache. Let's explore how it works:
Cache Control
You can customize caching behavior with fetchPolicy
:
const { data } = useQuery(GET_BOOKS, {
fetchPolicy: 'network-only', // 'cache-first' is the default
});
Common fetch policies:
cache-first
: Check the cache first; if the data isn't there, make a network requestnetwork-only
: Always make a network request and ignore cached datacache-only
: Only check the cache, never make a network requestcache-and-network
: Return cached data immediately, then update from network
Practical Example: Building a Book Library App
Let's put everything together in a comprehensive example of a book library application:
import React, { useState } from 'react';
import { ApolloClient, InMemoryCache, ApolloProvider, useQuery, useMutation, gql } from '@apollo/client';
// GraphQL queries and mutations
const GET_BOOKS = gql`
query GetBooks {
books {
id
title
author {
id
name
}
}
}
`;
const GET_AUTHORS = gql`
query GetAuthors {
authors {
id
name
}
}
`;
const ADD_BOOK = gql`
mutation AddBook($title: String!, $authorId: ID!) {
addBook(title: $title, authorId: $authorId) {
id
title
author {
id
name
}
}
}
`;
// Initialize Apollo Client
const client = new ApolloClient({
uri: 'https://your-graphql-endpoint.com/graphql',
cache: new InMemoryCache()
});
// Book List Component
function BookList() {
const { loading, error, data } = useQuery(GET_BOOKS);
if (loading) return <p>Loading books...</p>;
if (error) return <p>Error loading books: {error.message}</p>;
return (
<div>
<h2>Available Books</h2>
<ul className="book-list">
{data.books.map(book => (
<li key={book.id} className="book-item">
<div className="book-title">{book.title}</div>
<div className="book-author">by {book.author.name}</div>
</li>
))}
</ul>
</div>
);
}
// Add Book Form Component
function AddBookForm() {
const [title, setTitle] = useState('');
const [authorId, setAuthorId] = useState('');
const { data: authorsData, loading: authorsLoading } = useQuery(GET_AUTHORS);
const [addBook, { loading: addingBook }] = useMutation(ADD_BOOK, {
update(cache, { data: { addBook } }) {
try {
const { books } = cache.readQuery({ query: GET_BOOKS });
cache.writeQuery({
query: GET_BOOKS,
data: { books: [...books, addBook] },
});
} catch (e) {
// If the query isn't in the cache yet, don't do anything
console.error("Error updating cache:", e);
}
}
});
const handleSubmit = (e) => {
e.preventDefault();
if (!title || !authorId) return;
addBook({
variables: { title, authorId }
});
setTitle('');
setAuthorId('');
};
return (
<div className="add-book-form">
<h3>Add New Book</h3>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="title">Title:</label>
<input
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
disabled={addingBook}
required
/>
</div>
<div className="form-group">
<label htmlFor="author">Author:</label>
{authorsLoading ? (
<p>Loading authors...</p>
) : (
<select
id="author"
value={authorId}
onChange={(e) => setAuthorId(e.target.value)}
disabled={addingBook}
required
>
<option value="">Select an author</option>
{authorsData?.authors.map(author => (
<option key={author.id} value={author.id}>
{author.name}
</option>
))}
</select>
)}
</div>
<button
type="submit"
disabled={addingBook || !title || !authorId}
>
{addingBook ? 'Adding...' : 'Add Book'}
</button>
</form>
</div>
);
}
// Main App Component
function LibraryApp() {
return (
<div className="library-app">
<h1>GraphQL Book Library</h1>
<div className="app-container">
<BookList />
<AddBookForm />
</div>
</div>
);
}
// Root App Component with Apollo Provider
function App() {
return (
<ApolloProvider client={client}>
<LibraryApp />
</ApolloProvider>
);
}
export default App;
Advanced Features
Fragment Usage
Fragments help you reuse parts of your GraphQL queries:
import { gql, useQuery } from '@apollo/client';
// Define a fragment for book details
const BOOK_FRAGMENT = gql`
fragment BookDetails on Book {
id
title
pageCount
genre
}
`;
// Use the fragment in a query
const GET_BOOKS = gql`
query GetBooks {
books {
...BookDetails
author {
name
}
}
}
${BOOK_FRAGMENT}
`;
function BookList() {
const { loading, error, data } = useQuery(GET_BOOKS);
// Render component...
}
Local State Management
Apollo Client can manage local state alongside remote data:
const client = new ApolloClient({
uri: 'https://your-graphql-endpoint.com/graphql',
cache: new InMemoryCache(),
typeDefs: gql`
extend type Book {
isInWishlist: Boolean!
}
`,
resolvers: {
Mutation: {
toggleWishlist: (_, { id }, { cache }) => {
const bookFragment = gql`
fragment WishlistBook on Book {
id
isInWishlist
}
`;
const book = cache.readFragment({
id: `Book:${id}`,
fragment: bookFragment,
});
const updatedBook = {
...book,
isInWishlist: !book.isInWishlist,
};
cache.writeFragment({
id: `Book:${id}`,
fragment: bookFragment,
data: updatedBook,
});
return updatedBook;
}
}
}
});
Summary
In this tutorial, we've covered the essentials of Apollo Client for React:
- Setting up Apollo Client in a React application
- Querying data with the
useQuery
hook - Modifying data with the
useMutation
hook - Managing cache and updating UI after mutations
- Implementing optimistic UI for better user experience
- Handling errors from network and GraphQL operations
- Building a practical book library application
- Using advanced features like fragments and local state management
Apollo Client offers a powerful solution for working with GraphQL APIs in React applications. Its declarative approach to data fetching and state management integrates seamlessly with React's component model, making it an excellent choice for modern web applications.
Additional Resources
To continue your learning journey with React Apollo Client:
Exercises
- Extend the book library app to include a feature for deleting books.
- Add a feature to filter books by author or genre.
- Implement pagination for the book list using Apollo Client's fetchMore functionality.
- Create a detail view for books that loads when a user clicks on a book in the list.
- Add local state management to track books marked as "read" or "unread" without sending this data to the server.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)