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
orgetServerSideProps
- 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;