Take your Next js + GraphQL + TypeScript Setup to the next level
Integrating the full range of Next.js features with Apollo Client.
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
Install the following dependencies.
npm install lodash-es deepmerge
We will need these libraries to mutate the Apollo cache later on.
Install the types for
lodash-es
npm install --save-dev @types/lodash-es
Create a folder called
apollo
with anindex.ts
file inside.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 ifwindow
is defined or not.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 existingapolloClient
if it exists, or with a new instance ofcreateApolloClient()
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 theinitialState
usingmerge()
, and then restores the merged data into the cache. This allows the initial state to be used in any subsequent queries.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 calledpageProps
.The function then checks if the
pageProps
object has a property calledprops
. If it does, it adds the Apollo Client cache state to thepageProps.props
object using the identifierAPOLLO_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'sgetStaticProps/getServerSideProps
function to inject the Apollo Client cache state.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 calleduseApollo.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 identifierAPOLLO_STATE_PROP_NAME
. It then calls theinitializeApollo
function we created before to get either an existing or new instance of the Apollo client.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 theApolloProvider
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.