nextjs: persistent layouts with typescript

2022-03-27

 | 

~2 min read

 | 

382 words

NextJS has documentation for layouts. It even has a section for Typescript.

Unfortunately, when I tried to follow it, I ran into a whole host of issues.

Fortunately, I found enough clues online to solve it (and with help from the NextJS and Typescript discords respectively).

How does it work?

Well, let’s start with the _app.ts file:

// pages/_app.ts
import React from "react"

import { getLayout as getDefaultLayout } from "../components/SiteLayout"
import { AppPropsWithLayout } from "../types"

function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  const getLayout =
    Component.getLayout ??
    ((page: AppPropsWithLayout) => getDefaultLayout(page))

  return getLayout(<Component {...pageProps} />)
}

export default MyApp

The key point is that at this point, we’re still in line with the Next.JS documentation for Typescript, but that’s only because of the fallback with the getDefaultLayout.

The getDefaultLayout is pretty basic right now:

//components/SiteLayout.tsx
import Link from "next/link"
import React from "react"

export const SiteLayout = ({ children }: React.PropsWithChildren<unknown>) => (
  // apply any layout you want by default
  <div>{children}</div>
)

export const getLayout = (page: any) => <SiteLayout>{page}</SiteLayout>

export default SiteLayout

The point is that when you apply a getLayout to a exported component as a function, you need to use the same NextPageWithLayout:

For example, our Index.tsx:

//pages/Index.tsx
import Link from "next/link"
import { getLayout } from "../components/SiteLayout"
import { NextPageWithLayout } from "../types"

const Index: NextPageWithLayout<unknown> = () => (
  <div className="mt-8 max-w-xl mx-auto px-8">
    <h1 className="text-center">
      <span className="block text-xl text-gray-600 leading-tight">
        Welcome to this
      </span>
      <span className="block text-5xl font-bold leading-none">
        Awesome Website
      </span>
    </h1>
    <div className="mt-12 text-center">
      <Link href="/account-settings/basic-information">
        <a className="inline-block bg-gray-900 hover:bg-gray-800 text-white font-medium rounded-lg px-6 py-4 leading-tight">
          View account settings
        </a>
      </Link>
    </div>
  </div>
)

Index.getLayout = getLayout

export default Index

Where does that type come from? Well, we defined our own wrapper in types.d.ts (I’d love to know if this is the right place to define these sorts of types):

//types.d.ts
import { NextPage } from "next"
import { AppProps } from "next/app"
import { ReactElement, ReactNode } from "react"

export type NextPageWithLayout<T> = NextPage<T> & {
  getLayout?: (page: ReactElement) => ReactNode
}
export type AppPropsWithLayout = AppProps & {
  Component: NextPageWithLayout
}

I pulled together a simple repo (based on Adam Wathan’s blog post) to demonstrate how to adapt it to Typescript.



Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!