This Next.js pattern I've been using just got a whole lot better.
Posted by
Eelco Wiersma
@eelcodotdev
For the last two years I've been using a createPage
helper with Next.js that
allows you to re-use some common features, like fetching data, checking roles or feature flags, etc.
This idea was originally posted by
With the release of the app router in Next.js 13 I've been looking for a way to improve this pattern and make better use of RSC (React Server Components), but didn't find a setup yet that I was fully happy with.
Earlier this week I came across a tweet from
that showed the newdefineRoute
function that will be introduced in React Router (and Remix) when it dawned on me that I could use this
to improve my createPage
helper.
Here's an example how the new createPage
helper is used in the
import { notFound } from 'next/navigation'
import { z } from 'zod'
import { DashboardPage } from '#features/workspaces/dashboard'
import { createPage } from '#lib/create-page'
import { api } from '#lib/trpc/rsc.js'
const { Page, generateMetaData } = createPage({
params: z.object({
workspace: z.string(),
}),
searchParams: ['test'],
loader: async ({ params }) => {
const workspace = await api.workspaces.bySlug({
slug: params.workspace,
})
if (!workspace) {
notFound()
}
return {
workspace,
}
},
metadata: async ({ data }) => ({
title: `Dashboard - ${data.workspace.name}`,
}),
component: ({ params, searchParams, data }) => (
<DashboardPage
params={params}
searchParams={searchParams}
workspace={data.workspace}
/>
),
})
export { generateMetaData }
export default Page
So what's happening here?
The createPage
function is a factory function that returns a Page
component and metadata
or generateMetaData
that can be exported from an app router page.tsx
.
We can define params
and searchParams
using Zod schemas (or an array with the param names ['workspace']
).
These then become available in the loader
function that is called server-side to fetch the data for the metadata and page component.
In this example we're fetchin data from a tRPC procedure, but this could be any async function that returns the data needed for the page. The return value of the loader function is then inferred by the metadata, and component handlers.
The component returned by the createPage
is a React Server Component that wraps the DashboardPage
component, which can be a regular client component, or another RSC.
Now we no longer have to import and move around types, params and searchParams are validated, and established a common pattern for fetching initial data for our metadata and page components.
You can easily extend this helper with extra functionality, like role access.
const { Page, generateMetaData } = createPage({
roles: ['admin'],
component: ({ params, searchParams, data }) => (
<AdminPage params={params} searchParams={searchParams} data={data} />
),
})
You can
, there are still a few details I'm not certain about.safeParse
for the params and searchParams?component
be wrapped with Suspense
?cache
the loader function?Please do leave a comment (below the gist) if you have idea's, suggestions or other feedback.
Thanks for reading :)