Inject global data in Next.js during build time

 Reading time ~4 minutes

Heads up: this article is over a year old. Some information might be out of date, as I don't always update older articles.

Introduction

At work we started to use Next.js for a new project. So far the experience has been nothing but positive, however recently we hit one of the limitations of the framework. We wanted to inject data, fetched from an API, in all the pages during build time. Specifically we wanted to fetch Feature Flags from our dedicated service into Next.js, in order to conditionally render new features.

You would think that this requirement would be easily covered by Next.js, but unfortunately that’s not the case and you have to dig into several discussions to understand if a solution is even possible.

The issue

The original implementation (inherited from a previous SPA) to fetch feature toggles relied on a High-Order Component placed as high as possible in the Layout. The component was fetching (and caching) toggles at runtime, before rendering the children, but we immediately realized that such behavior was affecting static-site generation (SSG).

Therefore we decided to research how such toggles could be fetched at build time. To be fair we don’t need real-time visibility for our feature toggles since they do not change very often. However nothing stops us to revalidate the same feature toggles at runtime, for example to target specific users.

First Attempt

Next.js uses the App component to initialize pages. You can override it and control the page initialization.

The /pages/_app.js component sounded like the perfect place for fetching the data since, according to the documentation, it allows you to inject additional data into pages.

The following is the code of a custom App component, copy/pasted from the documentation

// import App from 'next/app'

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

// Only uncomment this method if you have blocking data requirements for
// every single page in your application. This disables the ability to
// perform automatic static optimization, causing every page in your app to
// be server-side rendered.
//
// MyApp.getInitialProps = async (appContext) => {
//   // calls page's `getInitialProps` and fills `appProps.pageProps`
//   const appProps = await App.getInitialProps(appContext);
//
//   return { ...appProps }
// }

export default MyApp

As you can see, it’s possible to uncomment the getInitialProps function and add custom logic to provide additional props to all pages. Something like

import App from 'next/app'

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

MyApp.getInitialProps = async (appContext) => {
  const appProps = await App.getInitialProps(appContext);

  const response = await fetch(`https://feature-toggles.com`);
  const toggles = await response.json();

  return { ...appProps, toggles }
}

export default MyApp

But it’s clear, from the comment above and from the documentation, that using getInitialProps in _app will opt-out all pages from automatic static optimization, which is Next.js ability to automatically determine if a page is static or not. This might not be a big deal, however we wanted to leverage the framework’s default behavior as much as possible, considering that we’re in the early stages of adoption.

Moreover, the App component currently does not support Next.js Data Fetching methods like getStaticProps or getServerSideProps.

Second Attempt

In Next.js it’s also possible to create a custom Document component, which is used to render a Page. However we found the same restrictions of the App component:

  • it does not support Next.js Data Fetching methods like getStaticProps or getServerSideProps
  • they explicitly recommend to avoid customizing getInitialProps

Third Attempt

The third attempt is the one that actually worked. Next.js can be configured through a next.config.js file (which is a regular Node.js module, not a JSON file). It’s used during the build phase and by the Next.js server, but it’s not included in the browser build.

/**
 * @type {import('next').NextConfig}
 */
const nextConfig = {
  /* config options here */
}

module.exports = nextConfig

However, exporting an async function rather than a static object, immediately triggered an error. By reading the documentation we immediately noticed that in Next.js versions above 12.0.10, module.exports = async () => is supported. So we were just a npm update away from the solution.

Here’s our final configuration.

const fs = require("fs-extra");

module.exports = async (phase, { defaultConfig }) => {
  const response = await fetch(`https://feature-toggles.com`);
  const toggles = await response.json();

  fs.writeJson(`toggles.json`, toggles);

  return {
    ...
  }
}

As you can see feature toggles are fetched and stored inside a toggles.json file that can be later imported in the App component and injected into each page:

import toggles from "toggles.json";

const App = ({ Component, pageProps }: AppProps) => {
  return (
    <>
      <Head>
        <title>{meta.title}</title>
        <meta name="description" content={meta.description} />
      </Head>
      <Layout>
        <Component {...pageProps} toggles={toggles} />
      </Layout>
    </>
  );
};

export default App;
comments powered by Disqus

Generate a temporary presigned upload URL using Laravel Storage class

Introduction

By default, all objects in an S3 bucket are private, meaning only the bucket account owner initially has access …