article·July 1, 2024

Next.js createPage helper with loader pattern

This Next.js pattern I've been using just got a whole lot better.

Posted by

EW

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

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

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.

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.

Please do leave a comment (below the gist) if you have idea's, suggestions or other feedback.

Thanks for reading :)