Apollo Client Cache Rehydration in Next.js

Kellen Mace Avatar

·

Apollo Client is a popular GraphQL client that can cache the results of your queries, among many other features. If you have an entirely client-side rendered (CSR) front-end app and want to do all your data fetching in the browser, you can simply start off with an empty Apollo Client cache and populate it with data as GraphQL requests execute.

But what if you want to use server-side rendering (SSR), static site generation (SSG), or incremental static regeneration (ISR), though? How can you fetch data ahead of time on the server? Can the Apollo Client’s cache initialize with that data on the client so that your queries don’t need to be rerun?

Thankfully, there is a way to fetch data on the server, populate Apollo Client’s cache, send that cache data to the client, and use it to “rehydrate” or initialize Apollo Client’s cache in the browser. Cache rehydration gives the benefits of SSR/SSG/ISR (a fully rendered HTML document on the initial page load, great SEO, etc.). Best of all, you still have the data in Apollo Client’s cache. This prevents your app from making unnecessary network requests for data you already have.

In this blog post, we’ll cover how to set up Apollo Client cache rehydration in a Next.js app.

Project Setup

Before setting up cache rehydration, fulfill the following prerequisites:

  1. Clone down the repo to your machine: https://github.com/kellenmace/apollo-client-cache-rehydration-in-next-js
  2. Run npm install (or yarn) to install the project’s dependencies.
  3. Create a .env.local file in the root directory with this variable defined: NEXT_PUBLIC_WORDPRESS_API_URL=http://my-local-site.local/graphql. Swap out http://my-local-site.local with the domain of a WordPress site that has WPGraphQL installed.
  4. Run npm run dev to get the app up and running locally.

Approach

Server-side Rendering (SSR) through the Apollo client follows this approach:

  1. Use Apollo Client’s getDataFromTree() function to get the results for all queries throughout your entire component tree for the current page.
  2. Extract Apollo Client’s cache and send that data to the client as a global window.APOLLO_STATE object.
  3. When Apollo Client initializes on the client, call its new InMemoryCache().restore() function, passing in the window.APOLLO_STATE object to seed it with the cache data from the server.

Although using the SSR approach works well with Next.js, the recommended way to do Apollo Client cache rehydration differs from that. We will instead do this:

  1. Use Next’s getStaticProps() or getServerSideProps() function to do our data fetching and populate Apollo Client’s cache.
  2. Extract Apollo Client’s cache and add it as a pageProps prop for the current page.
  3. When Apollo Client initializes on the client, call its new InMemoryCache().restore() function, passing in the cache data from the server.

For more information on how the Apollo client works, check out this official Next.js example project. We’ll use that same approach for our purposes, except the code examples will be written in TypeScript rather than JS, and all the data fetching examples will be specific to WordPress and WPGraphQL.

Install & Configure Apollo Client

Per Apollo Client’s getting started documentation, the @apollo/client and graphql NPM are already installed. The deepmerge and lodash packages have also been installed, which our project leverages.

To start configuring the client, open the lib/apolloClient.ts file in a code editor. It looks like this:

import { useMemo } from 'react';
import { ApolloClient, HttpLink, InMemoryCache, NormalizedCacheObject } from '@apollo/client';
import { relayStylePagination } from "@apollo/client/utilities";
import merge from 'deepmerge';
import isEqual from 'lodash/isEqual';

export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__';

let apolloClient: ApolloClient<NormalizedCacheObject> | undefined;

function createApolloClient() {
  return new ApolloClient({
    ssrMode: typeof window === 'undefined',
    link: new HttpLink({
      uri: process.env.NEXT_PUBLIC_WORDPRESS_API_URL, // Server URL (must be absolute)
      credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers`
    }),
    cache: new InMemoryCache({
      // typePolicies is not required to use Apollo with Next.js - only for doing pagination.
      typePolicies: {
        Query: {
          fields: {
            posts: relayStylePagination(),
          },
        },
      },
    }),
  })
}

export function initializeApollo(initialState: NormalizedCacheObject | null = null) {
  const _apolloClient = apolloClient ?? createApolloClient();

  // If your page has Next.js data fetching methods that use Apollo Client, the initial state
  // gets hydrated here
  if (initialState) {
    // Get existing cache, loaded during client side data fetching
    const existingCache = _apolloClient.extract();

    // Merge the existing cache into data passed from getStaticProps/getServerSideProps
    const data = merge(initialState, existingCache, {
      // combine arrays using object equality (like in sets)
      arrayMerge: (destinationArray, sourceArray) => [
        ...sourceArray,
        ...destinationArray.filter((d) =>
          sourceArray.every((s) => !isEqual(d, s))
        ),
      ],
    });

    // Restore the cache with the merged data
    _apolloClient.cache.restore(data);
  }

  // For SSG and SSR always create a new Apollo Client
  if (typeof window === 'undefined') return _apolloClient;

  // Create the Apollo Client once in the client
  if (!apolloClient) apolloClient = _apolloClient;

  return _apolloClient;
}

export function addApolloState(client: ApolloClient<NormalizedCacheObject>, pageProps: any) {
  if (pageProps?.props) {
    pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract();
  }

  return pageProps;
}

export function useApollo(pageProps: any) {
  const state = pageProps[APOLLO_STATE_PROP_NAME];
  const store = useMemo(() => initializeApollo(state), [state]);
  return store;
}
Code language: JavaScript (javascript)

Let’s walk through each of those function definitions.

createApolloClient()

This function is responsible for creating a new instance of Apollo Client.

We set Apollo Client’s ssrMode option to true if the code is running on the server, and to false if it’s running on the client.

The uri: process.env.NEXT_PUBLIC_WORDPRESS_API_URL line tells Apollo Client to use the GraphQL endpoint defined in your .env.local file for all network requests.

initializeApollo()

As its name suggests, this function initializes Apollo Client. It merges the initial state (data passed in from getStaticProps() / getServerSideProps()) with the existing client-side Apollo cache, then sets that new, merged data set as the new cache for Apollo Client.

addApolloState()

This function takes the pageProps returned from getStaticProps() / getServerSideProps() for the current page and adds to them Apollo’s cache data. From there, Next.js takes care of passing Apollo’s cache data, along with any other page-specific props into the page component.

useApollo()

This function calls initializeApollo() to get an instance of Apollo Client that has Apollo’s cache data added to it. This client is ultimately passed in as a prop to the ApolloProvider that Apollo Client provides.

Render an ApolloProvider

Inside of _app.tsx, we call useApollo() to get a new instance of Apollo Client, passing to it the pageProps for the current page. The apolloClient instance is then passed in as a prop to the ApolloProvider component, which wraps our application.

import { AppContext, AppInitialProps } from "next/app";
import { ApolloProvider } from "@apollo/client";
import { useApollo } from "../lib/apolloClient";

import "../styles/globals.css";

function MyApp({ Component, pageProps }: AppContext & AppInitialProps) {
  const apolloClient = useApollo(pageProps)

  return (
    <ApolloProvider client={apolloClient}>
      <Component {...pageProps} />
    </ApolloProvider>
  );
}

export default MyApp;
Code language: JavaScript (javascript)

How to Use in Page Components

Let’s see how we can use this Apollo Client cache rehydration superpower in our Next.js page components.

Open up pages/ssg.tsx. A simplified version of that file looks like this:

import { gql, useQuery } from "@apollo/client";
import { initializeApollo, addApolloState } from "../lib/apolloClient";

const POSTS_PER_PAGE = 10;

const GET_POSTS = gql`
  query getPosts($first: Int!, $after: String) {
    posts(first: $first, after: $after) {
      # etc.
    }
  }
`;

export default function SSG() {
  const { loading, error, data } = useQuery(GET_POSTS, {
    variables: {
      first: POSTS_PER_PAGE,
      after: null,
    }
  });

  // etc.
}

export async function getStaticProps(context: GetStaticPropsContext) {
  const apolloClient = initializeApollo();

  await apolloClient.query({
    query: GET_POSTS,
    variables: {
      first: POSTS_PER_PAGE,
      after: null,
    }
  });

  return addApolloState(apolloClient, {
    props: {},
  });
}
Code language: JavaScript (javascript)

Inside of Next.js’ getStaticProps() function, we call initializeApollo() to get an instance of Apollo Client that we can use for making GraphQL requests.

We then call the apolloClient.query() method, passing in the exact same query and variables that are used inside of our SSG page component when it calls the useQuery() hook. You’ll notice that we’re not even using the return value that apolloClient.query() provides. This is because we don’t need it – we’re only calling apolloClient.query() to populate Apollo Client’s cache with the data for the queries executed.

When our SSG page component is rendered in the browser and the useQuery() hook is called, Apollo Client will “see” that the data for the query being executed already exists it its cache, and will use that data instead of making a network request. This will result in the content on the page rendering immediately. Visit http://localhost:3000/ssg in your browser, open up the Network tab in the DevTools and refresh the page to confirm that no client-side GraphQL requests execute.

More info on Next.js’ getStaticProps() function can be found here: https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation.

SSR Page

Open up pages/ssr.tsx and visit http://localhost:3000/ssr. You can see that this file functions exactly the same as ssg.tsx, except that Next.js passes in the page props on every request since we’re using getServerSideProps() instead of getStaticProps(). Again, you can open up the Network tab in the DevTools and refresh the page to confirm that no client-side GraphQL requests execute.

More info on Next.js’ getServerSideProps() function can be found here: https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering.

CSR Page

To prove that we still have the option to do data fetching on the client, if desired, open up pages/csr.tsx and visit http://localhost:3000/csr. You can see that this page does not fetch any data on the server, since we’re not using getStaticProps() or getServerSideProps(). If you open up the Network tab in the DevTools and refresh the page, you can confirm that the GraphQL network requests execute client-side when the page first loads. You can do this if you want your request to run in the browser only, and not on the server.

Mixed SSG + CSR Page

Finally, open up pages/blog.tsx. Make sure you have eleven or more posts created in your WordPress backend, then visit http://localhost:3000/blog.You can see that inside of getStaticProps(), an Apollo Client query is executed to get the initial list of ten blog posts, which immediately get rendered to the page when it loads. If more posts exist in the WordPress backend, the user is presented with a Load more button. Clicking this button results in Apollo Client’s fetchMore() function being called, which fires off a network request to get the next five posts and appends them to the list. Once all posts have been loaded, the user sees an ✅ All posts loaded message.

The blog.tsx page shows how you can fetch some data on the server to populate Apollo’s cache. Also, the markup appears instantly when the page loads on the front-end. After, the page fires subsequent client-side requests to fetch additional data, adding it to Apollo’s cache..

How to Use this Code in Your Projects

In order to use Apollo Client for SSG/SSR/ISR in a Next.js project, you’ll need to follow these steps:

  1. Copy lib/apolloClient.ts into your project.
    As stated above, set the process.env.NEXT_PUBLIC_WORDPRESS_API_URL variable to reference the GraphQL endpoint of your headless WordPress backend.
    The object containing typePolicies that is being passed into new InMemoryCache() in this file can be omitted if you’re not doing any pagination in your project (such as the fetchMore() pagination demonstrated on the pages/blog.tsx page).
  2. In pages/_app.tsx, call useApollo(pageProps) to get an instance of Apollo Client, then pass that in as a prop to the <ApolloProvider /> component, as shown above.
  3. Mimic the page components in the /pages directory of this project, depending on whether you want your pages to do their data fetching at build time (see ssg.tsx), at request time on the server (see ssr.tsx), on the client only (see csr.tsx), or a mixture of those (see blog.tsx).

Wrapping Up

I hope this walk-through helps you with leveraging Apollo Client for SSG/SSR/ISR data fetching on your next Next.js project.

And that’s it! Thanks so much for giving this post a read, and please reach out to us with any questions you have! ????