Introduction
Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL. 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 that’s consistent with modern React practices.
Core Concepts
Key Components of Apollo Client
- Apollo Client: The core client instance that manages data and connects to GraphQL API
- Apollo Provider: The React component that makes Apollo Client available in your React component tree
- Apollo Cache: The normalized data store that manages query and mutation results (InMemoryCache)
- Apollo Link: The network layer that customizes request handling from client to GraphQL server
- React Hooks: Functional components for interacting with Apollo Client (useQuery, useMutation, etc.)
Setting Up Apollo Client
Basic Setup
import { ApolloClient, InMemoryCache, ApolloProvider, HttpLink } from '@apollo/client';
// Create an HTTP link
const httpLink = new HttpLink({
uri: 'https://your-graphql-endpoint.com',
});
// Create the Apollo Client instance
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
});
// Wrap your app with ApolloProvider
function App() {
return (
<ApolloProvider client={client}>
<YourApp />
</ApolloProvider>
);
}
Advanced Setup with Authentication
import { ApolloClient, InMemoryCache, ApolloProvider, HttpLink, from } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
// HTTP connection to the API
const httpLink = new HttpLink({ uri: 'https://your-graphql-endpoint.com' });
// Auth link for adding token to requests
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('token');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
}
};
});
// Combine the auth and http links
const client = new ApolloClient({
link: from([authLink, httpLink]),
cache: new InMemoryCache()
});
Querying Data
Basic Query with useQuery Hook
import { useQuery, gql } from '@apollo/client';
const GET_DOGS = gql`
query GetDogs {
dogs {
id
breed
name
}
}
`;
function Dogs() {
const { loading, error, data } = useQuery(GET_DOGS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.dogs.map(dog => (
<li key={dog.id}>{dog.name} ({dog.breed})</li>
))}
</ul>
);
}
Query with Variables
const GET_DOG = gql`
query GetDog($id: ID!) {
dog(id: $id) {
id
name
breed
}
}
`;
function DogProfile({ dogId }) {
const { loading, error, data } = useQuery(GET_DOG, {
variables: { id: dogId },
});
// Component logic...
}
Query Options
| Option | Description |
|---|---|
variables | An object containing all variables to be sent with the query |
fetchPolicy | Determines how data is retrieved from the cache (e.g., ‘cache-first’, ‘network-only’) |
errorPolicy | Determines how errors are handled (e.g., ‘none’, ‘all’, ‘ignore’) |
skip | Boolean to skip this query (useful for conditional queries) |
pollInterval | Number in ms for polling interval (refetch at regular intervals) |
notifyOnNetworkStatusChange | Boolean to trigger rerenders on network status changes |
context | Object passed to Apollo Links in the context property |
onCompleted | Callback executed when query completes successfully |
onError | Callback executed when the query errors |
Fetch Policies
| Policy | Description |
|---|---|
cache-first | Default. Checks cache first, then network if cache miss |
cache-only | Only checks cache, errors if data not found |
network-only | Always queries the server, never uses cache |
cache-and-network | Returns cached data then updates from network |
no-cache | Like network-only but doesn’t save to cache |
standby | Initial request behaves like cache-first, but doesn’t update on cache changes |
Mutations
Basic Mutation with useMutation Hook
import { useMutation, gql } from '@apollo/client';
const ADD_DOG = gql`
mutation AddDog($name: String!, $breed: String!) {
addDog(name: $name, breed: $breed) {
id
name
breed
}
}
`;
function AddDogForm() {
const [name, setName] = useState('');
const [breed, setBreed] = useState('');
const [addDog, { data, loading, error }] = useMutation(ADD_DOG);
const handleSubmit = (e) => {
e.preventDefault();
addDog({ variables: { name, breed } });
};
// Form JSX and logic...
}
Updating the Cache After Mutation
const [addDog] = useMutation(ADD_DOG, {
update(cache, { data: { addDog } }) {
// Read existing dogs from the cache
const { dogs } = cache.readQuery({ query: GET_DOGS });
// Update the cache with the new dog
cache.writeQuery({
query: GET_DOGS,
data: { dogs: [...dogs, addDog] },
});
}
});
Optimistic Updates
const [addDog] = useMutation(ADD_DOG, {
optimisticResponse: {
__typename: 'Mutation',
addDog: {
__typename: 'Dog',
id: 'temp-id',
name: formValues.name,
breed: formValues.breed,
}
},
update(cache, { data: { addDog } }) {
// Update cache logic...
}
});
Mutation Options
| Option | Description |
|---|---|
variables | Variables for the mutation |
optimisticResponse | The expected result used for optimistic UI |
refetchQueries | Queries to refetch after the mutation |
update | Function used to update the cache after the mutation |
onCompleted | Callback for when the mutation completes |
onError | Callback for when the mutation errors |
context | Context to be passed to Apollo Links |
fetchPolicy | How to interact with the cache |
Caching Strategies
Cache Configuration
const cache = new InMemoryCache({
typePolicies: {
User: {
// Unique identifier for User type
keyFields: ['id'],
fields: {
// Custom field policy for friends
friends: {
// Merge strategy for this field
merge(existing = [], incoming) {
return [...existing, ...incoming];
}
}
}
}
}
});
Manual Cache Operations
| Operation | Description | Example |
|---|---|---|
readQuery | Read data from cache without API call | const { dogs } = client.readQuery({ query: GET_DOGS }); |
writeQuery | Write data to cache manually | client.writeQuery({ query: GET_DOGS, data: { dogs: [...] } }); |
readFragment | Read a fragment from any object in cache | const dog = client.readFragment({ id: 'Dog:1', fragment: DOG_FRAGMENT }); |
writeFragment | Write a fragment to any object in cache | client.writeFragment({ id: 'Dog:1', fragment: DOG_FRAGMENT, data: dogData }); |
evict | Remove specific data from cache | client.cache.evict({ id: 'Dog:1' }); |
reset | Reset entire cache | client.resetStore(); |
Field Policies Types
| Policy | Purpose |
|---|---|
keyFields | Configure how to identify entities (instead of the default id or _id) |
merge | Define custom logic for merging incoming data with existing cached data |
read | Define custom logic for reading a field from the cache |
Error Handling
Error Policies
| Policy | Description |
|---|---|
none | Treat GraphQL errors as runtime errors (default) |
ignore | Ignore GraphQL errors and still display data |
all | Return both data and errors if they exist |
Common Error Patterns
// Component with error fallback
function DogList() {
const { loading, error, data } = useQuery(GET_DOGS);
if (loading) return <LoadingSpinner />;
if (error) {
// Network error
if (error.networkError) {
return <NetworkErrorMessage error={error.networkError} />;
}
// GraphQL errors
if (error.graphQLErrors) {
return (
<div>
{error.graphQLErrors.map(({ message }, i) => (
<ErrorBanner key={i} message={message} />
))}
</div>
);
}
return <GenericError message={error.message} />;
}
return <DogListDisplay dogs={data.dogs} />;
}
Local State Management
Local-Only Fields
// Query with both server and local fields
const GET_DOG_WITH_LOCAL_STATE = gql`
query GetDogWithLocalState($id: ID!) {
dog(id: $id) {
id
name
breed
# Local-only field (client-side)
isSelected @client
}
}
`;
TypePolicies for Local Fields
const cache = new InMemoryCache({
typePolicies: {
Dog: {
fields: {
isSelected: {
// Default value for isSelected field
read(_, { variables }) {
// Read from localStorage or return default
return localStorage.getItem(`selected_${variables.id}`) === 'true' || false;
}
}
}
}
}
});
Reactive Variables
import { makeVar, useReactiveVar } from '@apollo/client';
// Create a reactive variable
export const cartItemsVar = makeVar([]);
// In a component
function Cart() {
// Subscribe to changes
const cartItems = useReactiveVar(cartItemsVar);
function addToCart(item) {
// Update the variable - triggers re-renders anywhere useReactiveVar is used
cartItemsVar([...cartItemsVar(), item]);
}
// Component logic...
}
Performance Optimization
Techniques and Best Practices
| Technique | Description | Implementation |
|---|---|---|
| Query Deduplication | Batches identical queries made in the same tick | Enabled by default |
| Query Splitting | Split large queries into smaller ones | Create multiple focused queries |
| Pagination | Fetch data in chunks | Use fetchMore with existing query |
| Fragment Colocation | Define fragments close to components | Create fragments in component files |
| Prefetching | Load data before it’s needed | client.query() on hover or route change |
| Selective Polling | Update specific data at intervals | Use pollInterval on useQuery |
| Skip Queries | Prevent queries from running | Use skip: true option |
Example: Pagination with fetchMore
function DogList() {
const { loading, error, data, fetchMore } = useQuery(GET_DOGS, {
variables: { offset: 0, limit: 10 },
});
const loadMore = () => {
fetchMore({
variables: {
offset: data.dogs.length,
limit: 10,
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return {
dogs: [...prev.dogs, ...fetchMoreResult.dogs],
};
},
});
};
// Component logic with loadMore button...
}
Advanced Techniques
Using Apollo Link
import { ApolloClient, InMemoryCache, ApolloLink, HttpLink, from } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
import { RetryLink } from '@apollo/client/link/retry';
// Log operations
const loggerLink = new ApolloLink((operation, forward) => {
console.log(`Operation: ${operation.operationName}`);
return forward(operation);
});
// Handle errors
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.forEach(({ message, locations, path }) =>
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`)
);
if (networkError) console.log(`[Network error]: ${networkError}`);
});
// Retry failed requests
const retryLink = new RetryLink({
delay: {
initial: 300,
max: 3000,
jitter: true
},
attempts: {
max: 5,
retryIf: (error, _operation) => !!error
}
});
// HTTP link
const httpLink = new HttpLink({ uri: 'https://your-graphql-endpoint.com' });
// Combine links
const client = new ApolloClient({
link: from([loggerLink, errorLink, retryLink, httpLink]),
cache: new InMemoryCache()
});
Common Apollo Links
| Link | Purpose |
|---|---|
HttpLink | Connects to a GraphQL server over HTTP |
WebSocketLink | Connects to a GraphQL server over WebSocket for subscriptions |
RetryLink | Retries failed operations |
ErrorLink | Handles and modifies errors |
ApolloLink.from | Combines multiple links |
ApolloLink.split | Directs operations through different links based on criteria |
BatchHttpLink | Batches operations sent over HTTP |
ContextLink | Sets operation context |
Using Fragments for Component Reusability
const DOG_FRAGMENT = gql`
fragment DogDetails on Dog {
id
name
breed
displayImage
description
}
`;
const GET_DOG = gql`
query GetDog($id: ID!) {
dog(id: $id) {
...DogDetails
}
}
${DOG_FRAGMENT}
`;
// Components can import and use the fragment
function DogCard({ dog }) {
return (
<Card>
<Image src={dog.displayImage} />
<Heading>{dog.name}</Heading>
<Text>{dog.breed}</Text>
<Text>{dog.description}</Text>
</Card>
);
}
Common Challenges and Solutions
| Challenge | Solution |
|---|---|
| Stale data | Use fetchPolicy: ‘network-only’ or ‘cache-and-network’ |
| Cache inconsistency | Use cache.modify() to update related entities |
| Deep object updates | Use deep merge strategy in typePolicies |
| Race conditions | Implement concurrency control with versioning |
| Authentication expiration | Use Apollo Link to refresh tokens |
| Server schema changes | Update client-side queries and fragments |
| Large response size | Use GraphQL pagination and fragment optimization |
| Network failures | Implement retry strategies with RetryLink |
Best Practices
Organize GraphQL Operations: Store queries and mutations in separate files or near the components that use them.
Use Fragments: Break down complex queries into reusable fragments to improve component isolation.
Colocate Data Requirements: Define data needs close to the components that use them.
Consistent Error Handling: Develop consistent error handling patterns across your application.
Optimize Bundle Size: Use fine-grained imports to reduce bundle size:
// Good import { useQuery } from '@apollo/client'; // Better for tree-shaking (older versions) import { useQuery } from '@apollo/client/react/hooks';Avoid Overfetching: Request only the fields you need in each query.
Testing Strategy: Use MockedProvider for unit and integration tests:
import { MockedProvider } from '@apollo/client/testing'; const mocks = [ { request: { query: GET_DOGS, variables: { breed: 'Poodle' } }, result: { data: { dogs: [{ id: '1', name: 'Buck', breed: 'Poodle' }] } } } ]; render( <MockedProvider mocks={mocks} addTypename={false}> <DogList breed="Poodle" /> </MockedProvider> );Manage Local State: Use reactive variables for simpler global state management.
Implement Proper Authentication: Secure GraphQL endpoints and handle tokens appropriately.
Monitor Performance: Track query performance and optimize slow queries.
Resources for Further Learning
- Official Documentation: Apollo Client Documentation
- Apollo Client GitHub: Repository
- Tutorial: Apollo Client – Getting Started
- Apollo Graph Manager: Apollo Studio
- Community: Apollo Discord or Spectrum
- Blog: Apollo Blog
- YouTube: Apollo GraphQL YouTube Channel
- Courses:
