Take your Next js + GraphQL + TypeScript Setup
to the next level

Photo by SpaceX on Unsplash

Take your Next js + GraphQL + TypeScript Setup to the next level

Integrating the full range of Next.js features with Apollo Client.

Featured on Hashnode

This is a continuation of the Next js + GraphQL + TypeScript Setup article

Welcome back to our blog continuation! In this post, we'll show you how to take advantage of Next.js capabilities in combination with Apollo and GraphQL. Our focus will be on integrating Apollo Client with some of Next.js most essential features, including server-side rendering (SSR), static site generation (SSG), and incremental static regeneration (ISR). However, before we dive into the specifics, let's take a quick look at what each of these features involves.

What is SSR?

Server-Side Rendering (SSR) is a technique that allows you to pre-render your website's pages on the server and serve them to the client as HTML files. With SSR, the initial page load is faster because the server sends the HTML file with the rendered content, rather than sending a blank page that needs to be filled with content after the JavaScript loads.

In addition to improving initial page load times, SSR can also improve SEO, as search engines can easily crawl and index your pre-rendered pages. SSR can also help to optimize performance for users with slow internet connections or less powerful devices.

SSR can be implemented in many ways, but Next.js provides a built-in API that simplifies the process. By using Next.js, you can easily enable SSR for your React components, allowing you to take advantage of the benefits of SSR without needing to write a lot of server-side code.

What about SSG and ISR?

Static site generation (SSG) is a method of building websites that pre-renders all pages of a website at build time, and serves those pre-rendered pages to users, as opposed to generating pages on demand. While SSG is a fast and scalable approach to building websites, it has the limitation that every time you want to update any content, you need to rebuild the entire site. Incremental Static Regeneration (ISR) is a feature of Next.js that addresses this limitation by allowing you to update specific pages of your website without rebuilding the entire site.
It's important to note that SSG and ISR offer the benefits of SSR while also being more performant.

In the next sections, we will look at how you can combine SSR and ISR with GraphQL and Apollo Client to take your Next.js app to the next level.

Apollo's cache and SSR/SSG/ISR

Apollo Client provides a powerful cache system that allows you to manage the state of your application and access data quickly without having to make a network request. When you use Apollo Client to make a GraphQL query, the results are automatically stored in the cache, making subsequent requests for the same data much faster.

To achieve SSR/SSG/ISR in a Next.js app, you can make a network request on the server using Apollo Client and pre-populate the cache with the data. This allows the data to be immediately available on the client side, without the need for additional network requests.

By using the cache in combination with SSR/SSG/ISR, you can greatly improve the performance and user experience of your app, ensuring that data is always available quickly and reliably.

Here is a little diagram.

App Setup

To start I am going to use this boilerplate that has already TypeScript and Apollo setup in Next js.

Let's do this

  1. Install the following dependencies.

     npm install lodash-es deepmerge
    

    We will need these libraries to mutate the Apollo cache later on.

  2. Install the types for lodash-es

     npm install --save-dev @types/lodash-es
    
  3. Create a folder called apollo with an index.ts file inside.

  4. Inside this file, create a function that initializes the apollo client.

     import { ApolloClient, InMemoryCache } from '@apollo/client';
    
     const COUNTRIES_API = 'https://countries.trevorblades.com';
    
     function createApolloClient() {
       return new ApolloClient({
         ssrMode: typeof window === 'undefined',
         uri: COUNTRIES_API,
         cache: new InMemoryCache(),
       });
     }
    

    This should look familiar, now it's just abstracted into a function. Here we create an instance of an Apollo Client, the ssr property is very important here, it determines whether the client is being used in server-side rendering mode (true) or client-side rendering mode (false). All of this is based on if window is defined or not.

  5. Inside the same file create a function called initializeApollo with the following content.

     import merge from 'deepmerge';
     import isEqual from 'lodash-es/isEqual';
     import { ApolloClient, InMemoryCache } from '@apollo/client';
    
     let apolloClient: ApolloClient<NormalizedCacheObject> | null;
    
     function createApolloClient() {
     //...
     }
    
     export function initializeApollo(initialState?: any) {
       const _apolloClient = apolloClient ?? createApolloClient();
    
       if (initialState) {
         const existingCache = _apolloClient.cache.extract();
    
         const data = merge(initialState, existingCache, {
           arrayMerge: (destinationArray, sourceArray) => [
             ...sourceArray,
             ...destinationArray.filter((d) =>
               sourceArray.every((s) => !isEqual(d, s))
             ),
           ],
         });
         _apolloClient.cache.restore(data);
       }
    
       if (typeof window === 'undefined') {
         return _apolloClient;
       }
    
       if (!apolloClient) {
         apolloClient = _apolloClient;
       }
    
       return _apolloClient;
     }
    

    This function initializes the Apollo client. It creates a new variable called _apolloClient with an existing apolloClient if it exists, or with a new instance of createApolloClient() if it doesn't. This ensures that there is only one instance of the client throughout the application. If there is an initial state, it extracts the existing cache from _apolloClient, merges it with the initialState using merge(), and then restores the merged data into the cache. This allows the initial state to be used in any subsequent queries.

  6. The last function in this file will be addApolloState

     import merge from 'deepmerge';
     import isEqual from 'lodash/isEqual';
     import { ApolloClient, InMemoryCache } from '@apollo/client';
    
     let apolloClient: ApolloClient<NormalizedCacheObject> | null;
    
     export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__';
    
     function createApolloClient() {
       // ...
     }
    
     export function initializeApollo(initialState?: any) {
       // ...
     }
    
     export function addApolloState(
       client: ApolloClient<NormalizedCacheObject>,
       pageProps: any
     ) {
       if (pageProps?.props) {
         pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract();
       }
    
       return pageProps;
     }
    

    This utility function, called addApolloState, takes two parameters: an instance of an of Apollo client and an object containing props passed to a Next.js page called pageProps.

    The function then checks if the pageProps object has a property called props. If it does, it adds the Apollo Client cache state to the pageProps.props object using the identifier APOLLO_STATE_PROP_NAME.

    Finally, the function returns the pageProps object with the Apollo Client cache state added to it. This function will be used in a Next.js page's getStaticProps/getServerSideProps function to inject the Apollo Client cache state.

  7. Now let's create a hook that will help us get the Apollo client and pass it to our app. Create a folder called hooks with a file called useApollo.ts

     import { useMemo } from 'react';
    
     import { APOLLO_STATE_PROP_NAME, initializeApollo } from '../apollo';
    
     function useApollo(pageProps: any) {
       const state = pageProps[APOLLO_STATE_PROP_NAME];
       const client = useMemo(() => initializeApollo(state), [state]);
    
       return client;
     }
    
     export default useApollo;
    

    The hook first retrieves the Apollo Client cache state from pageProps using the identifier APOLLO_STATE_PROP_NAME. It then calls the initializeApollo function we created before to get either an existing or new instance of the Apollo client.

  8. Now we are finally ready to connect the fully pre-populated client with our app. Inside _app.tsx add the following code.

     import type { AppProps } from 'next/app';
     import { ApolloProvider } from '@apollo/client';
    
     import useApollo from '../hooks/useApollo';
    
     import '../styles/globals.css';
    
     function MyApp({ Component, pageProps }: AppProps) {
       const client = useApollo(pageProps);
       return (
         <ApolloProvider client={client}>
           <Component {...pageProps} />
         </ApolloProvider>
       );
     }
    
     export default MyApp;
    

    Now instead of creating the client instance here, we use the useApollo.ts hooks to get the client and pass it to the ApolloProvider

With this change, our Next.js app is now fully connected to our Apollo Client instance and can use GraphQL queries to fetch and display data.

Time to test

Now let's populate the countries query via getStaticProps

Inside pages/index.tsx add the following code.

import Head from 'next/head';
import { useQuery } from '@apollo/client';
import type { GetStaticProps } from 'next';

import QUERY_COUNTRIES from './queryCountries.graphql';

import { addApolloState, initializeApollo } from '../apollo';

import styles from '../styles/Home.module.css';

export default function Home() {
  // ...
}

export const getStaticProps: GetStaticProps = async (ctx) => {

    const client = initializeApollo();

    await client.query({
      query: QUERY_COUNTRIES
    });

    return addApolloState(client, {
      props: {},
    });
};

So here we initialize the client, make the query and pass the client, with a populated cache, to the addApolloState function along with the page props. We know that this function will add the apollo cache to the pageProps. This ensures that the data is pre-populated in the cache when the page is rendered on the client side.

Now, if you inspect the network tab, you should not see any loading and no requests being made by the useQuery hook on the client, as the data is already available in the cache.

In this example, we utilize the getStaticProps function to generate a static page, which enables SSG/ISR functionality. This approach can also be employed within the getServerSideProps function to provide us with SSR capabilities.

Conclusion

And that's about it!
We have now integrated the capabilities of Next.js with Apollo Client to take our app to the next level.

You can find the complete code here

Don't forget to like, share and star!

Thanks for reading and until the next time.