Next.js createPage helper with loader pattern
This Next.js pattern I've been using just got a whole lot better.
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 Rado Stankov and I've been using it ever since.
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 @matt_stobbs that showed the new
defineRoute
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.
How it works
Here's an example how the new createPage
helper is used in the Saas UI Pro starterkit.
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 }) => (<DashboardPageparams={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.
Extending the helper
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} />),})
Source code
You can find the source code in this gist, there are still a few details I'm not certain about.
- Use
safeParse
for the params and searchParams? - Should the
component
be wrapped withSuspense
? - Can we hydrate the data returned from the loader function easily to React Query.
cache
the loader function?- Turn this into a library?
Please do leave a comment (below the gist) if you have idea's, suggestions or other feedback.
Thanks for reading :)
Was this helpful?