How to create layout in Next.js from external source
September 17, 2020
Background
Next.js has upped its static generation game over the past year, something that has resulted in a lot of developers switching over from Gatsby. I am one of them.
While I enjoy the additional capabilities of Next.js, there were certain things that Gatsby did very well. One such feature was the data fetching at build time. By getting the data before building the pages, the information is already available and doesn't require additional requests.
One of these quirks that I ran into while developing with Next.js was a persistent, global layout, defined in an external headless CMS (in my case Sanity.io). Gatsby would fetch the data at build time, making the data available across the entire codebase. Next.js, however, does not. Instead, Next.js generates each page in isolation, fetching the data defined in getStaticProps
and passing it on to the component.
This behaviour led to two things:
- I had to request the global layout data on each page.
- With
fallback: true
Next.js sends off the requests ingetStaticPaths
at runtime and allows you to show a fallback component while the data loads. This fallback component didn't have access to the layout as it hadn't yet resolved.
The issue with the fallback component was only evident if you visited the route directly, without going from another existing page.
It's currently not possible to use getStaticProps
in _app
, something that could have taken care of the issues above. See this discussion thread on Github for more information.
This head-scratcher made me wish Next.js had something similar to gatsby-node
, allowing you to fetch the data at build time and store it in a file, any component could then read that file directly.
Well, it turns out you can achieve just that after all.
Next.js exposes a way to hook into the build step and run custom code. However, I'm sure this wasn't the intended use case, and I hope that we'll one day get a method similar to gatsby-node
to run scripts during build time for this purpose.
Enough preamble, let's get into it.
1. Create/modify Next.config.js
You'll need to create a next.config.js
file if it doesn't exist (in the root) and hook into the Webpack config function. From what I've found, it's the only piece that's context-aware of whether the code runs on the server or client-side.
Add the following code to the export of next.config.js
:
module.exports = {
webpack: (config, { isServer }) => {
if (isServer) {
require('./scripts/generate-layout')();
}
return config;
},
};
During the webhook cycle, you check if the current context is server-side if so you require and run the exported function right away. Then make sure to return the config at the end of the function.
2. Fetch layout and save as JSON
Create a new JavaScript file that fetches the layout. You can place it where you feel it makes sense, but the import path above points to a scripts
folder and a file named generate-layout.js
.
What about TypeScript I hear you ask? Unfortunately, that's not on the cards at this stage. At least not without extra effort. See this issue on Github for additional information regarding that.
I won't give you the exact code for this file, because it depends on your external source. But it would look something like this:
module.exports = async () => {
// fetch from wherever you've stored the layout
const layout = await getLayout();
// save the result the public folder
fs.writeFileSync('public/layout.json', JSON.stringify(layout));
};
The gist of it is to fetch the data, do whatever transforms you want on it first, then save it as a JSON file in the public folder.
3. Read JSON file
Next.js allows you to import JSON files directly in your React code. I added my import in _app
in case I need to override it with a different layout on sub-pages anywhere in the app. But if you know it won't ever change, you should be ok to import the JSON file directly in your layout component. Just like so:
// this path is relative to _app,
// you may need to update depending on where you import
import layout from '../public/layout.json';
Tip
You can avoid re-rendering the layout component by wrapping it in memo
from React. That way it won't re-render unless anything changes.
Caveat
Apart from the lack of TypeScript support the main caveat with this solution is that you will need to restart the dev server or rebuild the site when the layout changes at the source.
Improvement
You can still fetch the layout on each page if you want to ensure you get the most recent updates without needing to restart. That way the JSON layout becomes a backup and you won't need to restart the server when the layout changes. You have access to fs
in getStaticProps
, so you could even overwrite the old layout with the new one when it changes. The possibilities are endless.
Summary
This solution is a workaround that lets you hook into the build cycle of Next. It's not ideal since it sort of muscles in on Webpack, but so far my – admittedly limited – experience with it has been alright. Build times haven't increased noticeably.
Ideally, we'll get a solution that's similar to gatsby-node
, or a way to fetch global data at build time that is then available to the whole app. But until then, we'll have to make do with what we've got.