We just launched our new Hygraph Studio in beta. Learn more

GraphQL

Caching

In this tutorial, we'll provide an overview of client-side caching with GraphQL and show you how to implement it using the popular Apollo GraphQL client.

Caching involves storing data in cache memory to accelerate its retrieval in the future. This is particularly useful for applications that repeatedly fetch data from external sources, such as APIs, as it reduces the number of API requests and improves application performance.

In this tutorial, we'll provide an overview of client-side caching with GraphQL and show you how to implement it using the popular Apollo GraphQL client. We’ll also explain how caching is done in Hygraph.

Understanding Client-Side Caching

Client-side caching is the process of storing data in the client's memory or storage rather than fetching it from the server every time it's needed.

Client-side caching is especially powerful in GraphQL because it can store and retrieve the results of previous queries.

When a GraphQL query is executed, the client sends the query to the server and waits for a response. If the query has been executed before and its result is still in the cache, the client can retrieve the result from the cache instead of sending another request to the server. Furthermore, caching with GraphQL offers numerous advantages, including enhanced performance by decreasing network requests, reducing network traffic, and minimizing data transfer.

Implementing Client-Side Caching with Apollo Client

As the Apollo client is one of the most commonly used GraphQL clients, we will demonstrate our client-side caching implementations using it. However, implementations in other GraphQL clients are similar, albeit with slightly varying syntax.

Configuring the Apollo Client Cache

To configure the Apollo Client cache, we need to create a new instance of the InMemoryCache class and pass it to the ApolloClient constructor:

import { ApolloClient, InMemoryCache } from '@apollo/client';
const client = new ApolloClient({
uri: 'https://example.com/graphql',
cache: new InMemoryCache(),
});

This creates a new instance of the ApolloClient class with a new instance of the InMemoryCache class, a built-in cache provided by Apollo Client that can be used to store the results of GraphQL queries.

The InMemoryCache also has several options allowing us to customize its behavior. Here are some of the most commonly used options:

  • typePolicies: This option allows us to define custom merge functions for specific data types.
  • addTypename: This option adds the __typename field to every object in the cache. This can be useful for debugging and working with certain Apollo Client features, such as typePolicies.
  • resultCaching: This option enables caching of query results based on their variables. This can improve performance by reusing cached results for queries executed with the same variables.

These are just a few of the options supported by InMemoryCache. Consider checking out the official Apollo Client documentation for a complete list of options.

Using the Apollo Client Cache

Once we have configured the Apollo Client cache, we can store and retrieve the results of GraphQL queries without performing any other action. For example:

import { gql, useQuery } from "@apollo/client";
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`;
function Users() {
const { loading, error, data } = useQuery(GET_USERS);
if (loading) return "Loading...";
if (error) return `Error! ${error.message}`;
return (
<ul>
{data.users.map((user) => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
);
}

Since we’ve configured our graphql client cache, running the code above will retrieve the data from the cache if it's available and only fetch it from the server if it's not.

Bypassing Cache

Sometimes, we may need to bypass the cache and fetch the data from the server even if it's already available in the cache. To do this, we can set a query fetchPolicy option to no-cache or network-only:

const { loading, error, data } = useQuery(GET_USERS, {
fetchPolicy: 'no-cache', // OR network-only
});

Persisting Cache

By default, the Apollo Client cache is stored in memory, meaning it will be cleared when the page is refreshed or when the user closes the browser. However, we can also persist the cache to local storage or a server to ensure that it's available across sessions:

import { InMemoryCache } from '@apollo/client/cache';
import { persistCache } from 'apollo3-cache-persist';
const cache = new InMemoryCache();
persistCache({
cache,
storage: window.localStorage,
});

This code creates a new instance of the InMemoryCache class and uses the persistCache function from the apollo3-cache-persist package to persist the cache to local storage. When the user refreshes the page or closes and reopens the browser, the cached data will still be available.

You can also use apollo-cache-persist to integrate with other storage libraries like AsyncStorage, Ionic Storage, and several others.

Resetting Cache

To reset the cache and clear all of the cached data, we can use the resetStore method of the ApolloClient class:

import { useApolloClient } from '@apollo/client';
function LogoutButton() {
const client = useApolloClient();
function handleLogout() {
client.resetStore();
}
return <button onClick={handleLogout}>Logout</button>;
}

In this example, we use the useApolloClient hook to access the ApolloClient instance and then call the resetStore method when the user clicks the logout button.

Implementing Cache with Mutations

So far, we have discussed how to use client-side caching with queries. However, we can also use caching with mutations to improve the performance of our applications.

To implement cache with mutations in Apollo Client, we can use the update option that is available when we call the useMutation hook. The update function allows us to manually update the cache after a mutation has been completed instead of re-fetching the data from the server.

Suppose we have a mutation that enables users to add new posts to our application. When a user creates a new post, we aim to update the list of posts that is shown in our application without having to refetch the entire list from the server. To achieve this, we can implement a cache with mutations by manually updating the cache with the newly created post.

import { useMutation } from '@apollo/client';
import { ADD_POST } from './mutations';
function AddPostForm() {
const [addPost, { loading }] = useMutation(ADD_POST, {
update(cache, { data: { addPost } }) {
cache.modify({
fields: {
posts(existingPosts = []) {
const newPostRef = cache.writeFragment({
data: addPost,
fragment: gql`
fragment NewPost on Post {
id
title
body
}
`
});
return [...existingPosts, newPostRef];
}
}
});
}
});
// ...
}

In this example, we define a mutation called ADD_POST that adds a new post to the server. We then use the useMutation hook to call this mutation and pass in an update function.

The update function adds the newly created post to the posts field in the cache by calling the cache.writeFragment function. This function writes the new post to the cache using a custom fragment that defines the fields that should be written.

Using the update function guarantees that the cache is updated with the mutation's results, even if the mutation modifies data that is not directly queried by our application.

Hygraph Caching

Hygraph employs a caching strategy that utilizes globally distributed edge caches to serve all Content APIs. When a query is sent to the content API, its response is cached at multiple points of presence (POPs) across the globe.

This approach ensures that Hygraph manages all aspects of caching for you, so there's no need for you to be concerned about it.

Hygraph's Caching abilities is based on the content API endpoints you use. Hygraph comes with two different content API endpoints served by two Content-Delivery-Network (CDN) providers. Regular read & write endpoint: This endpoint caches all requests, and mutations invalidate the cache. It offers read-after-write consistency within a region and can take up to 30 seconds to update all caching nodes worldwide. While it's suitable for global content delivery, its caching logic is basic. High performance read-only endpoint: This uses a state-of-the-art caching approach, is faster, with continual improvements on cache invalidation logic. This endpoint uses model and stage-based invalidation, so only models affected by mutations are invalidated, ensuring the rest of the content remains cached.

Screenshot 2023-11-02 at 9.21.23 AM.png

Understanding Hygraph's Cache Consistency Modes

To gain a better understanding of both endpoints in Hygraph, it is important to have a basic knowledge of cache consistency modes.

Note: You can be assured that any changes made in Hygraph are successfully persisted if your response was successful.

Hygraph offers two cache consistency modes:

Eventual Consistency: In this mode, some changes may not be immediately visible after an update. However, they will be distributed around the globe with a short delay. Read-after-Write Consistency: This mode guarantees that any changes made through updates, deletions, or creations can be seen immediately through subsequent reads. However, this mode is only valid for operations that hit the same Point-of-Presence (POP).

You can read more about caching in Hygraph in this official documentation.

Conclusion

In this article, we have explored the significance of implementing client-side caching and how to implement it using Apollo Client. We have also discussed how caching is done with Hygraph meaning you don’t have to do anything.