Component-level static props in Next.js
One of Next.js' most useful features is that it pre-renders apps. By executing our code before React renders in the browser, Next.js produces static HTML documents that can be cached by CDNs and displayed without any client-side JavaScript running.
To help simplify pre-rendering, Next.js requires that dynamic data is prepared at the page-level. Components inside a page can't define their own static props or server side props. They can of course be dynamic, but Next.js will pre-render them in their initial state. Usually we pass the props prepared during pre-render down the tree to the components that need them.
┌──────────────────────┐ ╔════════════╗
│ │ ║ ║
│ getStaticProps() │───────▶║ <Page /> ║
│ │ ║ ║
└──────────────────────┘ ╚════════════╝
│
┌─────────────┴───────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ │ │ │
│ <Component /> │ │ <Component /> │
│ │ │ │
└─────────────────┘ └─────────────────┘
│
┌────────┘
▼
┌─────────────────┐
│ │
│ <Component /> │
│ │
└─────────────────┘
The flow of props in a statically rendered Next.js page.
This pattern works great, but wouldn't it be neat if we could add static props from individual components? Think about static queries in Gatsby. They can be dropped into the component tree at any level to fetch data. The page itself doesn't need to be aware of all the data required by the components within.
Shu, a developer at Vercel, shows us that this can be achieved with Next.js. The Next-CMS project provides a <CMS />
component that can be used to fetch and statically insert data from WordPress in any component.
Next-CMS uses Next.js' Static Site Generation (SSG) via the getStaticProps
function, and that is what we will focus on in this post.
Static sites are a great option for displaying content from a CMS, because it's usually unnecessary to fetch fresh data for every request. Next.js' Incremental Static Regeneration (ISR) feature allows static pages to be regenerated in the background, so we don't have to worry about stale content. This is kind of a superpower when compared to Gatsby's build model!
The <CMS />
component accepts an endpoint
prop to define its data source, and the fetched data is passed into a child render function. The assigned endpoint is automatically lifted up to the page's getStaticProps
function and fetched when the page is pre-rendered.
const Header = () => (
<CMS endpoint='/wp-json'>
{data => (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
</div>
)}
</CMS>
)
In this example from the Next-CMS docs, the <Header />
component will be pre-rendered using the site data retrieved from WordPress. The data is fetched only at pre-render—never on the client—and rendered to static HTML. We benefit from all the advantages of getStaticProps
, because indirectly that's what we are using.
The inner workings of Next-CMS
Next-CMS doesn't add any special capabilities to Next.js. Instead, it cleverly uses double rendering to automatically lift the requested endpoints up to the page's getStaticProps
function. The fetched data then gets injected at render time using React Context.
There are three parts to this technique.
1. Lifting and resolving API requests
A function is provided that gathers and fetches the endpoints assigned to nested <CMS />
components. It's called from inside the getStaticProps
function, and uses ReactDOM's renderToStaticMarkup
to perform an extra page render prior to Next.js' pre-render.
During this extra render, the endpoints of all nested <CMS />
components are pushed to a global array. They are added to a Set
—which is a convenient way to deduplicate them—and then resolved in parallel using fetch
.
┌─────────────────────────────────────┐ ╔════════════╗
│ getStaticProps() │ ║ ║
┌─▶│ │───────▶║ <Page /> ║
│ │ global.__next_ssg_requests = [] │ ║ ║
│ └─────────────────────────────────────┘ ╚════════════╝
│ ▲ │
│ │ ┌─────────────────────────┴──────────────┐
│ ┌──────────────────┘ ▼ ▼
│ │ ┌─────────────────────────────────────────────────┐ ┌─────────────────┐
│ │ │ <CMS endpoint='/wp-json' /> │ │ │
│ └──│ │ │ <Component /> │
│ │ global.__next_ssg_requests.push('/wp-json') │ │ │
│ └─────────────────────────────────────────────────┘ └─────────────────┘
│ │
│ ┌────────────────────┘
│ ▼
│ ┌─────────────────────────────────────────────────────────────┐
│ │ <CMS endpoint='/wp-json/wp/v2/posts' /> │
└───────────────────│ │
│ global.__next_ssg_requests.push('/wp-json/wp/v2/posts') │
└─────────────────────────────────────────────────────────────┘
Gathering static props before pre-render. Each CMS component pushes its assigned endpoint to a global array. Once the required endpoints have been lifted up to the getStaticProps function, they are fetched in parallel. The fetched data is passed into the page props when Next.js pre-renders the page.
The fetched data is added to an object, where its key is the requested endpoint. This object is passed into the page props. Because the key is the requested endpoint, it can be mapped back to the relevant <CMS />
component when the page is rendered.
2. React Context
The withCMSPage
higher order component wraps the page in a React Context. The Context is initialised using the fetched data from the page props. <CMS />
components nested at any level can then read the data returned from their assigned endpoint.
┌──────────────────────┐ ╔════════════╗
│ │ ║ ║
│ getStaticProps() │───────▶║ <Page /> ║
│ │ ║ ║
└──────────────────────┘ ╚════════════╝
│
│
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━│━━━━━━━━━━━━━━━━━┓
┃ │ ┃
┃ <CMSContext.Provider value={props.__next_ssg_data || {}} /> ├─────┐ ┃
┃ │ │ ┃
┃ │ │ ┃
┃ ┌─────────────────────────────────┘ │ ┃
┃ │ │ ┃
┃ ▼ ▼ ┃
┃ ┌─────────────────────────────────────┐ ┌─────────────────┐ ┃
┃ │ <CMS endpoint='/wp-json' /> │ │ │ ┃
┃ │ │ │ <Component /> │ ┃
┃ │ const data = context['/wp-json'] │ │ │ ┃
┃ └─────────────────────────────────────┘ └─────────────────┘ ┃
┃ │ ┃
┃ ┌────────────────────┘ ┃
┃ ▼ ┃
┃ ┌─────────────────────────────────────────────────────────────┐ ┃
┃ │ <CMS endpoint='/wp-json/wp/v2/posts' /> │ ┃
┃ │ │ ┃
┃ │ const data = context['/wp-json/wp/v2/posts'] │ ┃
┃ └─────────────────────────────────────────────────────────────┘ ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
The fetched data is injected into the CMS components using React Context.
3. The CMS component
The <CMS />
component runs in two different modes.
During the extra render, it simply pushes its assigned endpoint to a global array. It doesn't render any UI, because the extra render is performed only to lift required endpoints up to the getStaticProps
function.
During a normal render, the <CMS />
component reads the fetched data from Context, and then passes it down to its child render function.
There's a few connected parts to think about here. The important thing is that it's relatively simple for users to set up. The withCMSPage
and getCMSStaticProps
function are all we need to add to start using the <CMS />
component.
The inner workings are useful to understand if you want to apply this technique yourself. It's an interesting way to assemble a page because, beyond data fetching, it can be used to move work from the client render stage to the pre-render stage.
This website, for example, transforms code blocks to static HTML during pre-render, meaning the browser doesn't have to download and run a highlighter such as Prism.js. Manually adding every code block to getStaticProps
would be very cumbersome, but automatically lifting the code blocks up to getStaticProps
makes it convenient. (More about static code block rendering soon!)
I can see this pattern being really useful in Next.js apps, but it is a little fiddly. A small wrapper library could help solve that, or it could even be incorporated into Next.js itself.