Skip to content

GraphQL - Apollo Client

Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL. It’s the most popular choice for building client applications that interact with GraphQL APIs, especially within the React ecosystem, but it also supports other frameworks like Angular, Vue, and vanilla JS.

While you can use basic fetch calls, Apollo Client offers significant advantages like caching, UI integration via hooks, error handling, pagination, optimistic UI, and more.

  • ApolloClient Instance: The central object that manages configuration, network requests, and the cache.
  • ApolloProvider: A React component that uses React’s Context API to make the ApolloClient instance available to all components in your application tree.
  • gql Tag: A JavaScript template literal tag used to parse GraphQL query strings into a standard AST (Abstract Syntax Tree) format.
  • Hooks (useQuery, useMutation, useSubscription, useLazyQuery): React hooks that provide a declarative way to fetch, modify, and subscribe to GraphQL data within your functional components.
  • InMemoryCache: The default, powerful caching solution. It normalizes your data based on object identifiers (__typename and id or custom keys) and stores it in a flat structure, enabling efficient caching and automatic UI updates.
  • Apollo Link: A system for controlling the network flow of GraphQL operations. Links can be chained together to add features like authentication headers, error handling, retries, batching, etc.

Let’s revisit the setup process, highlighting key configuration aspects.

npm install @apollo/client graphql

In your application’s entry point (e.g., src/main.jsx or src/index.js):

// src/main.jsx or index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import {
ApolloClient,
InMemoryCache,
ApolloProvider,
createHttpLink // Often used for more complex link setups
} from '@apollo/client';
// import { setContext } from '@apollo/link-context'; // If using auth headers
// 1. Configure the HTTP Link (points to your GraphQL server)
const httpLink = createHttpLink({
uri: 'http://localhost:9000/graphql', // Replace with your server URL
});
// 2. (Optional) Configure Authentication Link (Example)
// const authLink = setContext((_, { headers }) => {
// const token = localStorage.getItem('authToken');
// return {
// headers: {
// ...headers,
// authorization: token ? `Bearer ${token}` : '',
// }
// }
// });
// 3. Configure the Cache
const cache = new InMemoryCache({
// Optional cache configuration
// typePolicies: { ... } // For advanced cache control
});
// 4. Create the Apollo Client instance
const client = new ApolloClient({
// Combine links if needed (e.g., authLink before httpLink)
// link: authLink.concat(httpLink),
link: httpLink, // Use httpLink directly if no authLink
cache: cache,
});
// 5. Wrap your App with ApolloProvider
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>
);

This setup initializes the client with the server endpoint and the cache, then makes the client available throughout the app.

The useQuery hook is the primary way to fetch data.

import { useQuery, gql } from '@apollo/client';
const GET_STUDENTS = gql`
query GetAllStudents {
students {
id
firstName
lastName
college {
id
name
}
}
}
`;
function StudentList() {
const { loading, error, data, refetch } = useQuery(GET_STUDENTS, {
// --- Common Options ---
// variables: { /* ... */ }, // If the query takes variables
// pollInterval: 5000, // Refetch data every 5 seconds
// fetchPolicy: 'cache-and-network', // Strategy for cache/network interaction
// notifyOnNetworkStatusChange: true, // Useful for showing loading indicators on refetch
// onError: (err) => { console.error("Query error:", err); },
// onCompleted: (d) => { console.log("Query completed:", d); }
});
if (loading) return <p>Loading students...</p>;
if (error) return <p>Error loading students: {error.message}</p>;
return (
<div>
<h2>Student List</h2>
<button onClick={() => refetch()}>Refetch Students</button>
<ul>
{data.students.map(student => (
<li key={student.id}>
{student.firstName} {student.lastName} (College: {student.college?.name || 'N/A'})
</li>
))}
</ul>
</div>
);
}

Key return values and options:

  • loading: Boolean, true while the query is in flight.
  • error: An ApolloError object if the request failed.
  • data: An object matching the shape of your query, containing the results (or undefined until loaded).
  • refetch: A function to manually re-execute the query.
  • fetchPolicy: Controls how Apollo interacts with the cache and network (e.g., cache-first, network-only, cache-and-network, no-cache). See docs for details. Default: cache-first.
  • pollInterval: Automatically refetches the query at a specified interval (in milliseconds).

The useMutation hook is used for sending mutations.

import React, { useState } from 'react';
import { useMutation, gql } from '@apollo/client';
const CREATE_STUDENT = gql`
mutation AddStudent($input: CreateStudentInput!) {
createStudent(input: $input) {
# Request needed fields of the created student for cache updates
id
firstName
lastName
collegeId
# Potentially request related data too
# college { id name }
}
}
`;
// Assume GET_STUDENTS query from useQuery example exists
function AddStudentForm() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [collegeId, setCollegeId] = useState('col-101'); // Example default
const [addStudent, { data, loading, error }] = useMutation(CREATE_STUDENT, {
// --- Common Options ---
// Option 1: Refetch queries after mutation
// refetchQueries: [{ query: GET_STUDENTS }],
// Option 2: Update cache directly (more complex but faster UI update)
update(cache, { data: { createStudent: newStudent } }) {
try {
// Read the existing students list from the cache
const existingData = cache.readQuery({ query: GET_STUDENTS });
if (existingData && newStudent) {
// Write the updated list back to the cache
cache.writeQuery({
query: GET_STUDENTS,
data: { students: [...existingData.students, newStudent] },
});
}
} catch (e) {
console.warn("Cache update failed (query likely not in cache yet):", e);
}
},
onError: (err) => { console.error("Mutation error:", err); },
// onCompleted: (d) => { console.log("Mutation completed:", d); }
});
const handleSubmit = (e) => {
e.preventDefault();
addStudent({
variables: {
input: { firstName, lastName, collegeId }
}
});
// Clear form fields
setFirstName('');
setLastName('');
};
return (
<form onSubmit={handleSubmit}>
<h3>Add New Student</h3>
{/* Input fields for firstName, lastName, collegeId */}
<input value={firstName} onChange={e => setFirstName(e.target.value)} placeholder="First Name" required/>
<input value={lastName} onChange={e => setLastName(e.target.value)} placeholder="Last Name" />
<input value={collegeId} onChange={e => setCollegeId(e.target.value)} placeholder="College ID" required/>
<button type="submit" disabled={loading}>
{loading ? 'Adding...' : 'Add Student'}
</button>
{error && <p>Error adding student: {error.message}</p>}
{data && <p>Added: {data.createStudent.firstName} (ID: {data.createStudent.id})</p>}
</form>
);
}

Key aspects:

  • useMutation returns a tuple: [mutateFunction, { data, loading, error }].
  • The mutateFunction (here addStudent) is called to trigger the mutation, usually passing a variables object.
  • Cache Updates: After a mutation modifies data, the Apollo cache needs to be updated. Common strategies:
    • refetchQueries: Easiest way. Re-runs specified queries after the mutation succeeds. Can be slow if queries are large.
    • Direct Cache Update (update function): More efficient. Manually read from and write to the cache using cache.readQuery, cache.writeQuery, cache.modify, etc. Requires understanding the cache structure but provides instant UI updates.
    • Mutation Return Value: Ensure your mutation returns the modified object with id and __typename so Apollo can potentially update the cache automatically for that specific object.
  • useLazyQuery: Like useQuery, but fetching is triggered manually by calling a function returned by the hook.
  • useSubscription: Used for subscribing to real-time updates from the server (requires WebSocket setup).
  • Pagination: Helpers for fetching paginated data (e.g., fetchMore).
  • Optimistic UI: Updating the UI immediately based on the expected mutation result, before the server confirms.
  • Local State Management: Using Apollo Client to manage client-side state alongside remote data.

The Apollo Client documentation is comprehensive and provides detailed guides on all features: