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!
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
e.g.Layout
component insidewithLayout
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"
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