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/client graphql 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.
Create a file called apolloClient.js
inside the lib
folder and write the following code
import fetch from 'isomorphic-unfetch'import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";import { setContext } from "@apollo/client/link/context";import { onError } from "@apollo/client/link/error";import { WebSocketLink } from "@apollo/client/link/ws";import { SubscriptionClient } from 'subscriptions-transport-ws'import auth0 from './auth0';let accessToken = nullconst requestAccessToken = async () => {if (accessToken) returnconst 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 serverconst resetTokenLink = onError(({ networkError }) => {if (networkError && networkError.name === 'ServerError' && networkError.statusCode === 401) {accessToken = null}})const createHttpLink = (headers) => {const httpLink = new HttpLink({uri: 'https://ready-panda-91.hasura.app/v1/graphql',credentials: 'include',headers, // auth token is fetched on the server sidefetch,})return httpLink;}const createWSLink = () => {return new WebSocketLink(new SubscriptionClient('wss://ready-panda-91.hasura.app/v1/graphql', {lazy: true,reconnect: true,connectionParams: async () => {await requestAccessToken() // happens on the clientreturn {headers: {authorization: accessToken ? `Bearer ${accessToken}` : '',},}},}))}export default function createApolloClient(initialState, headers) {const ssrMode = typeof window === 'undefined'let linkif (ssrMode) {link = createHttpLink(headers)} else {link = createWSLink()}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 Hasura Cloud. So don't forget to change the uri to match your Hasura instance in Hasura Cloud.
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:
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 doneconst 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 nullif (typeof ctx.req === 'undefined') return nullconst s = await auth0.getSession(ctx.req)if (s && s.accessToken == null) return nullreturn {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-sideif (!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 ssrclient = 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 developmentif (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 methodslet 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 renderif (ctx.res && ctx.res.finished) {return pageProps}// Only if ssr is enabledif (ssr) {try {// Run all GraphQL queriesconst { getDataFromTree } = await import('@apollo/react-ssr')await getDataFromTree(<AppTreepageProps={{...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-errorconsole.error('Error while running `getDataFromTree`', error)}// getDataFromTree does not call componentWillUnmount// head side effect therefore need to be cleared manuallyHead.rewind()}}// Extract query data from the Apollo storeconst 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.
- Build apps and APIs 10x faster
- Built-in authorization and caching
- 8x more performant than hand-rolled APIs