Frontend Serverless with React and GraphQL

Start at the beginning and work your way through this project. The code for each step as well as the finished project can be found in the Github repository.


11. Add WithApollo

Objective: Add Apollo to the base of our project which will manage all the data fetching.

First let's install all the npm packages that we will need. With versions of apollo past version 1 they split the functionality into a variety of different packages so we will need to import all of them that we will need for this application. In our case we will need the client, in memory cache, and a data fetching layer using HTTP since that's how we will access our backend.

bash

npm install --save @apollo/react-hooks apollo-client apollo-cache-inmemory apollo-link-http isomorphic-unfetch @apollo/react-ssr graphql

Next we will create a <WithApollo> component which we will use as a higher order component in our _app.tsx file. The cool thing about the _app.tsx file is that it will be loaded on every page that we visit in our pages directory.

We are basing the <WithApollo> component off of an official Vercel Next.js example from the following repository in the file located in lib/apollo.ts.

https://github.com/vercel/next.js/tree/ff2d28c4ff0a2cc1ce408817f2714eebf1cf72c2/examples/with-apollo

components/WithApollo.tsx

import React from 'react';
import App from 'next/app';
import Head from 'next/head';
import { ApolloProvider } from '@apollo/react-hooks';
import createApolloClient from '../utils/apolloClient';
// 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;
};
/**
* 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, ctx) => {
// 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, ctx);
}
// Reuse client on the client-side
if (!globalApolloClient) {
globalApolloClient = createApolloClient(initialState, ctx);
}
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 = false } = {}) => (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);
}
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 inAppContext = Boolean(ctx.ctx);
const { apolloClient } = initOnContext(ctx);
// Run wrapped getInitialProps methods
let pageProps = {};
if (PageComponent.getInitialProps) {
pageProps = await PageComponent.getInitialProps(ctx);
} else if (inAppContext) {
pageProps = await App.getInitialProps(ctx);
}
// Only on the server:
if (typeof window === 'undefined') {
const { AppTree } = ctx;
// When redirecting, the response is finished.
// No point in continuing to render
if (ctx.res && ctx.res.finished) {
return pageProps;
}
// Only if dataFromTree is enabled
if (ssr && AppTree) {
try {
// Import `@apollo/react-ssr` dynamically.
// We don't want to have this in our client bundle.
const { getDataFromTree } = await import('@apollo/react-ssr');
// Since AppComponents and PageComponents have different context types
// we need to modify their props a little.
let props;
if (inAppContext) {
props = { ...pageProps, apolloClient };
} else {
props = { pageProps: { ...pageProps, apolloClient } };
}
// Take the Next.js AppTree, determine which queries are needed to render,
// and fetch them. This method can be pretty slow since it renders
// your entire AppTree once for every query. Check out apollo fragments
// if you want to reduce the number of rerenders.
// https://www.apollographql.com/docs/react/data/fragments/
await getDataFromTree(<AppTree {...props} />);
} 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();
}
}
return {
...pageProps,
// Extract query data from the Apollo store
apolloState: apolloClient.cache.extract(),
// Provide the client for ssr. As soon as this payload
// gets JSON.stringified it will remove itself.
apolloClient: ctx.apolloClient,
};
};
}
return WithApollo;
};

Next wwe have to create the createApolloClient which we will put in the utils directory. Be sure to call the getConfig method so that we can use the BRANCH, GRAPHCMSURL, and GRAPHCMSPROJECTID environmental variables for the uri parameter.

utils/apolloClient.ts

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import fetch from 'isomorphic-unfetch';
import getConfig from 'next/config';
const { publicRuntimeConfig } = getConfig();
const { BRANCH, GRAPHCMSURL, GRAPHCMSPROJECTID } = publicRuntimeConfig.graphcms;
export default function createApolloClient(initialState, ctx) {
// The `ctx` (NextPageContext) will only be present on the server.
// use it to extract auth headers (ctx.req) or similar.
return new ApolloClient({
ssrMode: Boolean(ctx),
link: new HttpLink({
uri: `${GRAPHCMSURL}/${GRAPHCMSPROJECTID}/${BRANCH}`, // Server URL (must be absolute)
credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers`
fetch,
}),
cache: new InMemoryCache().restore(initialState),
});
}

Finally, we have to call the withApollo method from the App component:

/pages/_app.tsx

import 'antd/dist/antd.css';
import App from 'next/app';
import { withApollo } from '../components/WithApollo';
class MyApp extends App {
render() {
const { Component, pageProps } = this.props;
return <Component {...pageProps} />;
}
}
export default withApollo({ ssr: true })(MyApp);