Persistent layout in Next.js with TypeScript and HOC

Viewed 12324

I want to add a persistent layout to certain pages of my Next.js application. I found this article explaining a couple ways on how someone could do this. It seems pretty straightforward, however I have encountered the following two problems when using the recommended way of doing it:

  1. I am using TypeScript and am not sure how to type it. For example, I have the following, which is working, but I obviously don't like using as any:
const getLayout =
    (Component as any).getLayout ||
    ((page: NextPage) => <SiteLayout children={page} />);
  1. I am using Apollo and so I am using a withApollo HOC (from here) for certain pages. Using this causes Component.getLayout to always be undefined. I don't have a good enough understanding of what is going on to know why this is happening (I can guess), so it's difficult to solve this by myself.

Since asking this question they have added a good example to their documentation

4 Answers

I have the similar problem and this is how I solved it for my project.

Create a types/page.d.ts type definition:

import { NextPage } from 'next'
import { ComponentType, ReactElement, ReactNode } from 'react'

export type Page<P = {}> = NextPage<P> & {
  // You can disable whichever you don't need
  getLayout?: (page: ReactElement) => ReactNode
  layout?: ComponentType
}

In your _app.tsx file,

import type { AppProps } from 'next/app'
import { Fragment } from 'react'
import type { Page } from '../types/page'

// this should give a better typing
type Props = AppProps & {
  Component: Page
}
const MyApp = ({ Component, pageProps }: Props) => {
  // adjust accordingly if you disabled a layout rendering option
  const getLayout = Component.getLayout ?? (page => page)
  const Layout = Component.layout ?? Fragment

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

  // or swap the layout rendering priority
  // return getLayout(<Layout><Component {...pageProps} /></Layout>)
}

export default MyApp

The above is just a sample implementation best suited for my use-case, you can switch the type in types/page.d.ts to fit your needs.

You can extend Next type definitions by adding getLayout property to AppProps.Component.

next.d.ts (as of Next 11.1.0):

import type { CompletePrivateRouteInfo } from 'next/dist/shared/lib/router/router';
import type { Router } from 'next/dist/client/router';

declare module 'next/app' {
  export declare type AppProps = Pick<CompletePrivateRouteInfo, 'Component' | 'err'> & {
    router: Router;
  } & Record<string, any> & {
    Component: {
      getLayout?: (page: JSX.Element) => JSX.Element;
    }
  }
}

pages/_app.tsx:

import type { AppProps } from 'next/app';

const NextApp = ({ Component, pageProps }: AppProps) => {
  // Typescript does not complain about unknown property
  const getLayout = Component.getLayout || ((page) => page);
  // snip
}

If you want to have even more type-safety, you can define custom NextPageWithLayout type which will assert that your component has getLayout property set.

next.d.ts (as of Next 11.1.0):

import type { NextPage } from 'next';
import type { NextComponentType } from 'next/dist/next-server/lib/utils';

declare module 'next' {
  export declare type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
    getLayout: (component: NextComponentType) => JSX.Element;
  };
}

pages/example-page/index.tsx:

import type { NextPageWithLayout } from 'next';

const ExamplePage: NextPageWithLayout<ExamplePageProps> = () => {
  // snip
}

// If you won't define `getLayout` property on this component, TypeScript will complain
ExamplePage.getLayout = (page) => {
  // snip
}

Note that while extending Next type definitions you have to provide exact type/interface signature (together with matching generics), otherwise declaration merging won't work. This unfortunately means adjusting type definitions if library changes API.

  1. I believe next.js provides a Component prop as a part of AppProps. This example should have what you are looking for.
  2. You may need to assign getLayout after creating the component wrapped with withApollo:
import CustomLayout = CustomLayout;
const MyPage = () => (...);

const MyPageWithApollo = withApollo(MyPage);
MyPageWithApollo.Layout = CustomLayout;
export default MyPageWithApollo;

Additionally, next.js has two examples you can use to achieve exactly this (not written in typescript though):

  1. Dynamic App Layout
  2. With Apollo
// if no Layout is defined in page, use this
const Default: FC = ({children}) => <>{children}</>

function MyApp({Component, pageProps}: AppProps & {Component: {Layout: FC}}) {
  // this will make layput flexible. 
  // Layout is extracted from the page
  const Layout = Component.Layout ?? Default
  return (
      <Layout>
        <Component {...pageProps} />
      </Layout>
  )
}
export default MyApp

Let's say you want to use the Layout in "Home",

import { Layout } from "@components"

export default function Home({
     ...
}
Home.Layout = Layout
Related