How to Use Apollo Client for State Management in GraphQL Apps

Explore how to use Apollo Client for state management in GraphQL applications. Learn how to manage both remote and local data with Apollo’s built-in cache

GraphQL has revolutionized the way developers interact with APIs by offering flexible queries and reduced over-fetching. However, effective state management remains essential to building scalable, responsive applications. In GraphQL apps, where data flows constantly between the server and client, managing state efficiently is critical for performance, user experience, and maintaining a clean codebase.

One powerful tool for managing both remote and local state in a GraphQL-based application is Apollo Client. Apollo Client simplifies state management by allowing you to fetch, cache, and update data directly from your GraphQL API. But what many developers don’t realize is that Apollo Client can also be used to manage local state, making it a comprehensive solution for state management in GraphQL applications.

This article explores how to use Apollo Client to manage state in your GraphQL apps, from fetching and caching remote data to handling local state, implementing reactive variables, and using Apollo’s built-in cache for seamless state updates. By the end of this article, you’ll have a deep understanding of how Apollo Client can serve as the backbone of your state management strategy.

Why Apollo Client for State Management?

In most frontend applications, developers rely on two primary types of state:

Remote State: Data fetched from an API or backend service, such as user information, product listings, or other data from your GraphQL server.

Local State: Data specific to the client-side that isn’t stored on the server, such as UI state (whether a modal is open), form inputs, or application settings.

Apollo Client excels at managing both types of state. Traditionally, you would need a separate state management library (like Redux or MobX) to handle local state. But with Apollo Client, you can manage local state alongside your remote state in a unified way. This reduces the complexity of maintaining two different state management systems and results in a cleaner, more efficient codebase.

Here’s why Apollo Client is ideal for GraphQL apps:

Unified State Management: You can manage both remote and local state using the same APIs, without needing a separate library.

Normalized Caching: Apollo automatically caches fetched data, reducing redundant network requests and boosting app performance.

Reactive Variables: Apollo offers reactive variables for managing local state, which automatically trigger UI updates when state changes.

Powerful DevTools: Apollo DevTools allow you to inspect queries, check cache state, and debug your GraphQL queries.

Let’s dive into how you can use Apollo Client for state management, starting with its core capabilities.

Setting Up Apollo Client

Before we can use Apollo Client for state management, we need to integrate it into a GraphQL app. Here’s a quick overview of how to set up Apollo Client in a React application.

Installing Apollo Client

First, install the necessary dependencies using npm or yarn:

npm install @apollo/client graphql

Configuring Apollo Client

Once you’ve installed Apollo Client, the next step is to configure it to connect to your GraphQL API.

import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';

const client = new ApolloClient({
uri: 'https://your-graphql-api.com/graphql',
cache: new InMemoryCache(),
});

function App() {
return (
<ApolloProvider client={client}>
<YourComponent />
</ApolloProvider>
);
}

In this example, we’ve set up Apollo Client with an InMemoryCache to store the fetched data and provide it to the React component tree using the ApolloProvider. With this setup, we can now use Apollo Client to fetch data and manage state.

Managing Remote State with Apollo Client

At the core of Apollo Client’s functionality is its ability to manage remote state—data fetched from a GraphQL server. Apollo Client’s useQuery hook is a convenient way to query data and handle state related to loading, errors, and responses. Here’s how it works:

Example: Fetching Data with useQuery

Let’s start by fetching data from a GraphQL API using Apollo’s useQuery hook.

import { gql, useQuery } from '@apollo/client';

const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`;

function UserList() {
const { loading, error, data } = useQuery(GET_USERS);

if (loading) return <p>Loading...</p>;
if (error) return <p>Error fetching users</p>;

return (
<ul>
{data.users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
);
}

Here’s what happens in this code:

  1. The GET_USERS query is sent to the GraphQL server.
  2. Apollo automatically manages the loading and error states, so you can focus on the UI.
  3. The data object contains the response from the server, which we can use to render the UI.

This is Apollo Client in action, handling remote state for you. But what about caching and updating this state?

Normalized Caching with Apollo Client

Apollo Client’s InMemoryCache automatically caches the results of GraphQL queries. When you fetch data using a query, Apollo stores the data in its cache, ensuring that subsequent queries for the same data don’t result in redundant network requests. This improves performance and responsiveness, especially in larger apps with frequent data fetching.

Example: Using Cached Data

Apollo automatically reads from the cache whenever it has previously fetched the requested data. If you need to explicitly read from or write to the cache, Apollo gives you full control.

import { gql, useQuery } from '@apollo/client';

const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;

function UserProfile({ id }) {
const { loading, error, data } = useQuery(GET_USER, {
variables: { id },
});

if (loading) return <p>Loading...</p>;
if (error) return <p>Error fetching user</p>;

return <div>{data.user.name} - {data.user.email}</div>;
}

In this example, Apollo automatically caches the user data fetched by the GET_USER query. If this user data is queried again in the future, Apollo will return the cached data rather than refetching it from the server.

Sometimes, you may want to manipulate the cache directly, such as updating local data after a mutation without refetching it from the server.

Manually Interacting with Apollo Cache

Sometimes, you may want to manipulate the cache directly, such as updating local data after a mutation without refetching it from the server. This is where the cache.writeQuery and cache.readQuery methods come in handy.

Example: Updating the Cache After a Mutation

const ADD_USER = gql`
mutation AddUser($name: String!, $email: String!) {
addUser(name: $name, email: $email) {
id
name
email
}
}
`;

function AddUserForm() {
const [addUser] = useMutation(ADD_USER, {
update(cache, { data: { addUser } }) {
const { users } = cache.readQuery({ query: GET_USERS });
cache.writeQuery({
query: GET_USERS,
data: { users: users.concat([addUser]) },
});
},
});

// Form handling code...
}

Here, after a user is added through the addUser mutation, we manually update the cache to include the newly added user, ensuring that the UI reflects this change without needing to refetch the entire list of users from the server.

Managing Local State with Apollo Client

While Apollo Client is great at managing remote state from your GraphQL server, it’s also a powerful tool for managing local state—data that’s specific to the client, such as form inputs, UI elements, or temporary data.

Using Reactive Variables for Local State

Apollo Client’s Reactive Variables provide a simple yet powerful mechanism for managing local state. Reactive variables can hold any piece of local data and are reactive, meaning that when the variable changes, any query or component that depends on it automatically re-renders.

Example: Defining and Using Reactive Variables

import { makeVar, gql, useQuery } from '@apollo/client';

// Define a reactive variable
const isLoggedInVar = makeVar(false);

const GET_LOGIN_STATUS = gql`
query GetLoginStatus {
isLoggedIn @client
}
`;

function LoginStatus() {
const { data } = useQuery(GET_LOGIN_STATUS);

return (
<div>
{data.isLoggedIn ? 'Logged In' : 'Logged Out'}
<button onClick={() => isLoggedInVar(!data.isLoggedIn)}>
Toggle Login Status
</button>
</div>
);
}

In this example:

  1. The isLoggedInVar reactive variable manages the local state of the login status.
  2. The query GET_LOGIN_STATUS reads the login status from the client-side state, using the @client directive.
  3. When the login status changes, the UI automatically re-renders to reflect the new state.

Reactive variables give you a flexible way to manage local state in your GraphQL app without needing a separate state management solution like Redux.

Using Local-Only Fields in Queries

In addition to reactive variables, Apollo Client allows you to add local-only fields to your GraphQL queries. This enables you to combine remote data from your GraphQL API with local state, all in one query.

Example: Combining Remote and Local Data

const GET_USER_WITH_THEME = gql`
query GetUserWithTheme {
user(id: "123") {
id
name
email
}
theme @client
}
`;

function UserProfileWithTheme() {
const { data } = useQuery(GET_USER_WITH_THEME);

return (
<div style={{ backgroundColor: data.theme }}>
<h2>{data.user.name}</h2>
<p>{data.user.email}</p>
</div>
);
}

In this query, we fetch both user data from the GraphQL server and the theme state from the local Apollo cache. This approach allows you to seamlessly combine remote and local state management in your components.

Using Apollo Client DevTools for Debugging

One of the standout features of Apollo Client is its powerful DevTools. With Apollo Client DevTools, you can inspect GraphQL queries, view the current cache state, and monitor mutations and subscriptions.

Installing Apollo Client DevTools

To start using Apollo DevTools, simply install the browser extension available for Chrome and Firefox. Once installed, you can inspect your app’s Apollo Client state in real time, helping you debug issues, analyze cache updates, and optimize your state management.

Features of Apollo DevTools

GraphQL Query Explorer: View all GraphQL queries sent from your app and inspect the returned data.

Cache Inspection: Visualize the state of the Apollo cache, see how data is normalized, and track changes in real time.

Mutation Logging: Monitor all mutations and their effects on the cache.

Apollo DevTools make it easier to understand how your app’s state is managed, both locally and remotely, and help you identify performance bottlenecks or data inconsistencies.

Advanced State Management Techniques with Apollo Client

While we’ve covered the basics of using Apollo Client for both remote and local state, let’s explore more advanced techniques that can enhance the efficiency, performance, and scalability of your GraphQL applications. These strategies will help you get the most out of Apollo Client by optimizing caching, managing subscriptions, handling large data sets, and integrating pagination.

1. Optimizing Apollo’s Cache for Performance

The Apollo Client cache is a powerful feature that helps reduce unnecessary network requests and improve app performance. However, managing cache effectively is crucial, especially in large applications where data can change frequently.

Normalizing Cache

Apollo Client’s InMemoryCache normalizes data, storing each entity by its unique identifier (id). This means if the same entity is requested by multiple queries, Apollo Client doesn’t duplicate the data but rather references the already-cached version. This speeds up subsequent queries and reduces memory usage.

You can configure cache normalization using the cache property during Apollo Client setup. If your GraphQL server doesn’t provide id fields for entities, you can specify a custom identifier.

Example: Custom Cache Key Function

const cache = new InMemoryCache({
typePolicies: {
User: {
keyFields: ["username"], // Use username as the unique identifier for users
},
},
});

const client = new ApolloClient({
uri: 'https://your-graphql-api.com/graphql',
cache,
});

In this example, instead of relying on the default id field, we configure Apollo to use username as the unique identifier for users. This can be useful if your API doesn’t expose id fields or if multiple identifiers are required.

Fine-tuning Cache Eviction

In some cases, you may need to remove or replace cached data manually. For instance, if a user logs out, you may want to clear the cached user data for security reasons. Apollo Client provides cache eviction methods to handle such scenarios.

client.cache.evict({ fieldName: 'user' });
client.cache.gc(); // Runs garbage collection to remove unused cache entries

Evicting cache ensures that stale data is cleared, preventing it from showing up in future queries.

2. Handling GraphQL Subscriptions for Real-Time Data

Apollo Client supports subscriptions, which are essential for real-time data updates in GraphQL apps. Subscriptions allow the server to push updates to the client whenever specific events happen, such as new chat messages, stock price updates, or order status changes.

Apollo Client supports subscriptions, which are essential for real-time data updates in GraphQL apps

Setting Up Apollo Client for Subscriptions

To use subscriptions, you need to configure Apollo Client to use a WebSocket link, which establishes a WebSocket connection between the client and server.

npm install @apollo/client subscriptions-transport-ws

Next, configure the WebSocket link:

import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { WebSocketLink } from 'subscriptions-transport-ws';
import { getMainDefinition } from '@apollo/client/utilities';

// Create an HTTP link for queries and mutations
const httpLink = new HttpLink({
uri: 'https://your-graphql-api.com/graphql',
});

// Create a WebSocket link for subscriptions
const wsLink = new WebSocketLink({
uri: 'wss://your-graphql-api.com/graphql',
options: {
reconnect: true, // Reconnect in case of connection loss
},
});

// Split between HTTP and WebSocket links based on the operation type
const link = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);

const client = new ApolloClient({
link,
cache: new InMemoryCache(),
});

In this example, we use the split function to determine whether a request is a query/mutation (which uses the HTTP link) or a subscription (which uses the WebSocket link).

Example: Subscribing to Real-Time Data

import { gql, useSubscription } from '@apollo/client';

const MESSAGE_SUBSCRIPTION = gql`
subscription OnMessageReceived {
messageReceived {
id
content
user {
id
name
}
}
}
`;

function MessageList() {
const { data, loading } = useSubscription(MESSAGE_SUBSCRIPTION);

if (loading) return <p>Loading messages...</p>;

return (
<ul>
{data.messageReceived.map((message) => (
<li key={message.id}>
<strong>{message.user.name}</strong>: {message.content}
</li>
))}
</ul>
);
}

Here, useSubscription listens for new messages in real time, ensuring that the UI automatically updates whenever a new message is received. Subscriptions are a powerful way to deliver real-time experiences, such as chat apps, dashboards, or live event tracking.

3. Managing Pagination with Apollo Client

Pagination is a common challenge in state management, especially when dealing with large data sets like product listings or user-generated content. Apollo Client simplifies pagination with its built-in caching and fetching policies.

There are two common pagination strategies:

Offset-based pagination: Fetches data in pages based on an offset and limit.

Cursor-based pagination: Uses a cursor to load additional data incrementally.

Example: Cursor-Based Pagination

import { gql, useQuery } from '@apollo/client';

const GET_POSTS = gql`
query GetPosts($after: String) {
posts(after: $after, first: 10) {
edges {
node {
id
title
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
`;

function PostList() {
const { data, fetchMore } = useQuery(GET_POSTS);

if (!data) return <p>Loading posts...</p>;

return (
<div>
<ul>
{data.posts.edges.map((post) => (
<li key={post.node.id}>{post.node.title}</li>
))}
</ul>
{data.posts.pageInfo.hasNextPage && (
<button
onClick={() => {
fetchMore({
variables: {
after: data.posts.pageInfo.endCursor,
},
});
}}
>
Load More
</button>
)}
</div>
);
}

In this example:

  1. The fetchMore function is used to fetch additional data when the user clicks “Load More”.
  2. The query uses cursor-based pagination, where the endCursor is passed to load the next set of results.
  3. Apollo automatically merges the fetched data with the existing data in the cache, making it easy to handle incremental loading of large data sets.

4. Error Handling and Retry Logic in Apollo Client

No application is immune to errors, and robust error handling is essential for delivering a good user experience. Apollo Client provides mechanisms to catch and handle errors gracefully, whether they occur during queries, mutations, or subscriptions.

Using onError for Global Error Handling

Apollo’s ApolloLink allows you to set up global error handling using the onError function. This is useful for dealing with issues like authentication failures, network errors, or specific GraphQL errors.

import { ApolloClient, InMemoryCache, ApolloLink, HttpLink } from '@apollo/client';
import { onError } from '@apollo/client/link/error';

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}`);
}
});

const httpLink = new HttpLink({ uri: 'https://your-graphql-api.com/graphql' });

const client = new ApolloClient({
link: ApolloLink.from([errorLink, httpLink]),
cache: new InMemoryCache(),
});

This onError link logs any GraphQL or network errors globally, allowing you to handle or log these issues for debugging or user notification purposes.

Retry Logic

If a query or mutation fails due to a network issue, you might want to retry the request automatically. You can use the Apollo Retry Link to implement automatic retries for network failures.

npm install apollo-link-retry
import { RetryLink } from 'apollo-link-retry';

const retryLink = new RetryLink({
attempts: { max: 3, retryIf: (error) => !!error },
delay: { initial: 300, max: 2000, jitter: true },
});

const client = new ApolloClient({
link: ApolloLink.from([retryLink, errorLink, httpLink]),
cache: new InMemoryCache(),
});

In this example, failed requests are automatically retried up to three times with a delay between attempts. This ensures that temporary network issues don’t disrupt the user experience.

5. Advanced Cache Management: Writing and Modifying Cache Data

In more complex applications, you may need to directly interact with Apollo’s cache to update data in response to mutations or other events. This can be useful for things like optimistic UI updates, where you want to reflect a change in the UI before the server confirms it.

Example: Optimistic UI Updates

Optimistic updates allow your app to feel more responsive by assuming the mutation will succeed and updating the UI immediately. If the mutation fails, the UI can revert to the previous state.

const ADD_COMMENT = gql`
mutation AddComment($postId: ID!, $content: String!) {
addComment(postId: $postId, content: $content) {
id
content
}
}
`;

function CommentForm({ postId }) {
const [addComment] = useMutation(ADD_COMMENT, {
optimisticResponse: {
addComment: {
id: 'temp-id',
content: 'This is an optimistic comment',
__typename: 'Comment',
},
},
update(cache, { data: { addComment } }) {
const { comments } = cache.readQuery({ query: GET_COMMENTS, variables: { postId } });
cache.writeQuery({
query: GET_COMMENTS,
data: { comments: comments.concat([addComment]) },
});
},
});

// Form handling code...
}

In this example:

  1. The optimisticResponse simulates the new comment being added to the UI before the server confirms the mutation.
  2. The cache is updated immediately, reflecting the new comment, ensuring the app remains responsive even during slow network requests.

Conclusion

Apollo Client provides an all-in-one solution for state management in GraphQL applications, allowing you to handle both remote and local state with ease. By using features like the useQuery hook, reactive variables, and the powerful Apollo cache, you can build scalable, performant apps without the complexity of integrating multiple state management libraries.

Whether you’re building a simple UI with local-only fields or a complex app that syncs data with a GraphQL backend, Apollo Client offers the flexibility and power you need to manage state effectively. By adopting Apollo Client for state management, you can streamline your development process, improve performance, and maintain a clean, maintainable codebase.

At PixelFree Studio, we specialize in building high-performance web applications using tools like Apollo Client and GraphQL. If you’re looking to optimize state management in your GraphQL app or need help implementing Apollo Client, contact us today to see how we can help!

Read Next: