Configure Apollo Client with Next.js

Apollo gives a neat abstraction layer and an interface to your GraphQL server. You don't need to worry about constructing your queries with request body, headers and options, that you might have done with axios or fetch say. You can directly write queries and mutations in GraphQL and they will automatically be sent to your server via your apollo client instance.

React Apollo Hooks Installation

Let's get started by installing apollo client & peer graphql dependencies:

$ yarn add apollo-boost @apollo/react-hooks graphql apollo-link-ws subscriptions-transport-ws

Create Apollo Client Instance

Now let's look at creating the Apollo Client Instance for the app. apollo-boost would have installed various dependencies that are used here.

Open lib/apolloClient.js and write the following code

import fetch from 'isomorphic-unfetch'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { HttpLink } from 'apollo-link-http'
import { onError } from 'apollo-link-error'
import { WebSocketLink } from 'apollo-link-ws'
import { SubscriptionClient } from 'subscriptions-transport-ws'
import auth0 from './auth0';
let accessToken = null
const requestAccessToken = async () => {
if (accessToken) return
const res = await fetch(`${process.env.APP_HOST}/api/session`)
if (res.ok) {
const json = await res.json()
accessToken = json.accessToken
} else {
accessToken = 'public'
}
}
// remove cached token on 401 from the server
const resetTokenLink = onError(({ networkError }) => {
if (networkError && networkError.name === 'ServerError' && networkError.statusCode === 401) {
accessToken = null
}
})
const createHttpLink = (headers) => {
const httpLink = new HttpLink({
uri: 'https://learn-hasura.herokuapp.com/v1/graphql',
credentials: 'include',
headers, // auth token is fetched on the server side
fetch,
})
return httpLink;
}
const createWSLink = () => {
return new WebSocketLink(
new SubscriptionClient('wss://learn-hasura.herokuapp.com/v1/graphql', {
lazy: true,
reconnect: true,
connectionParams: async () => {
await requestAccessToken() // happens on the client
return {
headers: {
authorization: accessToken ? `Bearer ${accessToken}` : '',
},
}
},
})
)
}
export default function createApolloClient(initialState, headers) {
const ssrMode = typeof window === 'undefined'
let link
if (ssrMode) {
link = createHttpLink(headers) // executed on server
} else {
link = createWSLink() // executed on client
}
return new ApolloClient({
ssrMode,
link,
cache: new InMemoryCache().restore(initialState),
})
}

Let's try to understand what is happening here.

HttpLink and InMemoryCache

We are creating an HttpLink to connect ApolloClient with the GraphQL server. As you know already, our GraphQL server is running on Heroku. So don't forget to change the uri to match your Hasura instance in Heroku.

At the end, we instantiate ApolloClient by passing in our HttpLink and a new instance of InMemoryCache (recommended caching solution). We are wrapping all of this in a function which will return the client. We are also determining the ssrMode and passing it to Apollo.

WebSocketLink and Client Side

We are creating WebSocketLink to be used on the client side. The Authorization headers will be generated by making a request to Auth0 API from the client.

Next let's create the HOC withApollo that we will use inside our Next.js page to wrap our page component.

Open lib/withApollo.js and add the following code:

githublib/withApollo.js
import React from 'react';
import App from 'next/app';
import Head from 'next/head';
import { ApolloProvider } from '@apollo/react-hooks';
import createApolloClient from './apolloClient';
import auth0 from './auth0';
// On the client, we store the Apollo Client in the following variable.
// This prevents the client from reinitializing between page transitions.
let globalApolloClient = null;
/**
* Installs the Apollo Client on NextPageContext
* or NextAppContext. Useful if you want to use apolloClient
* inside getStaticProps, getStaticPaths or getServerSideProps
* @param {NextPageContext | NextAppContext} ctx
*/
export const initOnContext = (ctx) => {
const inAppContext = Boolean(ctx.ctx);
// We consider installing `withApollo({ ssr: true })` on global App level
// as antipattern since it disables project wide Automatic Static Optimization.
if (process.env.NODE_ENV === 'development') {
if (inAppContext) {
console.warn(
'Warning: You have opted-out of Automatic Static Optimization due to `withApollo` in `pages/_app`.\n' +
'Read more: https://err.sh/next.js/opt-out-auto-static-optimization\n'
);
}
}
// Initialize ApolloClient if not already done
const apolloClient =
ctx.apolloClient ||
initApolloClient(ctx.apolloState || {}, inAppContext ? ctx.ctx : ctx);
// We send the Apollo Client as a prop to the component to avoid calling initApollo() twice in the server.
// Otherwise, the component would have to call initApollo() again but this
// time without the context. Once that happens, the following code will make sure we send
// the prop as `null` to the browser.
apolloClient.toJSON = () => null;
// Add apolloClient to NextPageContext & NextAppContext.
// This allows us to consume the apolloClient inside our
// custom `getInitialProps({ apolloClient })`.
ctx.apolloClient = apolloClient;
if (inAppContext) {
ctx.ctx.apolloClient = apolloClient;
}
return ctx;
};
async function getHeaders(ctx) {
if (typeof window !== 'undefined') return null
if (typeof ctx.req === 'undefined') return null
const s = await auth0.getSession(ctx.req)
if (s && s.accessToken == null) return null
return {
authorization: `Bearer ${s ? s.accessToken: ''}`
}
}
/**
* Always creates a new apollo client on the server
* Creates or reuses apollo client in the browser.
* @param {NormalizedCacheObject} initialState
* @param {NextPageContext} ctx
*/
const initApolloClient = (initialState, headers) => {
// Make sure to create a new client for every server-side request so that data
// isn't shared between connections (which would be bad)
if (typeof window === 'undefined') {
return createApolloClient(initialState, headers);
}
// Reuse client on the client-side
if (!globalApolloClient) {
globalApolloClient = createApolloClient(initialState, headers);
}
return globalApolloClient;
};
/**
* Creates a withApollo HOC
* that provides the apolloContext
* to a next.js Page or AppTree.
* @param {Object} withApolloOptions
* @param {Boolean} [withApolloOptions.ssr=false]
* @returns {(PageComponent: ReactNode) => ReactNode}
*/
export const withApollo = ({ ssr = true } = {}) => (PageComponent) => {
const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
let client;
if (apolloClient) {
// Happens on: getDataFromTree & next.js ssr
client = apolloClient;
} else {
// Happens on: next.js csr
// client = initApolloClient(apolloState, undefined);
client = initApolloClient(apolloState, {});
}
return (
<ApolloProvider client={client}>
<PageComponent {...pageProps} />
</ApolloProvider>
);
};
// Set the correct displayName in development
if (process.env.NODE_ENV !== 'production') {
const displayName =
PageComponent.displayName || PageComponent.name || 'Component';
WithApollo.displayName = `withApollo(${displayName})`;
}
if (ssr || PageComponent.getInitialProps) {
WithApollo.getInitialProps = async (ctx) => {
const { AppTree } = ctx
// Initialize ApolloClient, add it to the ctx object so
// we can use it in `PageComponent.getInitialProp`.
const apolloClient = (ctx.apolloClient = initApolloClient(null, await getHeaders(ctx)))
// Run wrapped getInitialProps methods
let pageProps = {}
if (PageComponent.getInitialProps) {
pageProps = await PageComponent.getInitialProps(ctx)
}
// Only on the server:
if (typeof window === 'undefined') {
// When redirecting, the response is finished.
// No point in continuing to render
if (ctx.res && ctx.res.finished) {
return pageProps
}
// Only if ssr is enabled
if (ssr) {
try {
// Run all GraphQL queries
const { getDataFromTree } = await import('@apollo/react-ssr')
await getDataFromTree(
<AppTree
pageProps={{
...pageProps,
apolloClient
}}
/>
)
} catch (error) {
// Prevent Apollo Client GraphQL errors from crashing SSR.
// Handle them in components via the data.error prop:
// https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
console.error('Error while running `getDataFromTree`', error)
}
// getDataFromTree does not call componentWillUnmount
// head side effect therefore need to be cleared manually
Head.rewind()
}
}
// Extract query data from the Apollo store
const apolloState = apolloClient.cache.extract()
return {
...pageProps,
apolloState
}
}
}
return WithApollo;
};

Note the usage of getHeaders where we are making a request to Auth0 to retrieve the accessToken.

We are going to make use of this HOC inside our root page.

Open pages/index.js and update the export by adding withApollo

import Header from '../components/Header'
import Login from "../components/Auth/Login";
import TodoPrivateWrapper from "../components/Todo/TodoPrivateWrapper";
import TodoPublicWrapper from "../components/Todo/TodoPublicWrapper";
import OnlineUsersWrapper from "../components/OnlineUsers/OnlineUsersWrapper";
import { useFetchUser } from '../lib/user'
+ import { withApollo } from '../lib/withApollo'
const IndexPage = () => {
const { user, loading } = useFetchUser()
if(loading) {
return <div>Loading...</div>
}
if (!loading && !user) {
return <Login />
}
return(
...
)
}
- export default IndexPage
+ export default withApollo()(IndexPage)

That's it. Our Next.js app is now configured with Apollo Client to talk to Hasura APIs.