Return to Homepage

Next.js Persistent Layouts in TypeScript

Published on: 2021-06-12

Based off the work that Adam Wathan did on persistent layouts in Next.js, I decided to take it a step further and add the complexity of TypeScript into the mix.

Adam's solution worked great for the most part, however I wanted the option to pass properties on a page-by-page basis when rendering a layout. With this in mind, I cooked up a small high-order component to do the job.

To start off, I needed a way to extend the AppProps type that Next.js provides with my own.

interface MyAppProps<P = {}> extends AppProps<P> {
    Component: NextComponentType<NextPageContext, any, P> & {
        Layout?: (page: React.ReactNode) => JSX.Element,
    }
}

Creating a new interface (in this case MyAppProps) that overrides the AppProps Component definition allows me to add additional properties to the Component prototype. This Layout function signature is 1:1 with what Adam explained in his post. (if it aint broke, dont fix it!)

Continuing on, similarly to what Adam explained in his post, we need to wrap the page component being rendered by Next.js with our layout component. Simple enough!

let NOOP = withLayout(
    ({ children }: React.PropsWithChildren<{}>) => <>{children}</>
)

function MyApp({ Component, pageProps, ...props }: MyAppProps) {
    let Layout = Component.Layout || NOOP
    
    // ...
    
    return (
        Layout(<Component {...pageProps} />)
    )
}

With all that boilerplate done away with, here is the fun part!

withLayout.tsx
export interface LayoutProps {
    title?: string
}

type WithLayout = (
    Layout: React.ComponentType<Omit<LayoutProps, 'children'>>,
    props?: React.ComponentPropsWithoutRef<typeof Layout>
) => (page: React.ReactNode) => React.ReactNode

export const withLayout: WithLayout = (Layout, props) => 
    (page) => <Layout {...props}>{page}</Layout>
Side Note:

You could go as far as to wrap the Layout component inside withLayout with a "Skeleton Layout" component that takes most/all of the base properties to avoid having to pass the base properties down each time in every "child layout"

e.g.
function BaseLayout({ children, title }) {
    return (
        <>
            <Head>
                <title>{title}</title>
            </Head>
            {children}
        </>
    )
}

export const withLayout: WithLayout = (Layout, props) => 
    (page) => (
        <BaseLayout {...props}>
            <Layout {...props}>
                {page}
            </Layout>
        </BaseLayout>
    )

The withLayout high-order component.

This allows us to define the layout component and specify the props that are based on a common set of props as defined by the LayoutProps interface.

A comparison:

IndexPage.Layout = page => <HomeLayout>{page}</HomeLayout>

To

IndexPage.Layout = withLayout(HomeLayout, {
    title: 'Home',
    // ...extra type-hinted props
})
Return to Homepage