DEV Community

Oskar Filipowicz
Oskar Filipowicz

Posted on • Edited on

Next.js Per-Page Layouts and TypeScript

Next.js allows developers to set up dynamic layouts per-page. Benefits and details of this approach can be read here. However, doing what is described there will generate some issues when we use TypeScript in strict mode.

What's wrong

Example code from the official documentation:

// pages/_app.tsx

export default function MyApp({ Component, pageProps }) {
  // Use the layout defined at the page level, if available
  const getLayout = Component.getLayout || ((page) => page)

  return getLayout(<Component {...pageProps} />)
}
Enter fullscreen mode Exit fullscreen mode

Generates following errors:

Parameter 'page' implicitly has an 'any' type.
Enter fullscreen mode Exit fullscreen mode
Property 'getLayout' does not exist on type 'NextComponentType<NextPageContext, any, {}>'.
  Property 'getLayout' does not exist on type 'ComponentClass<{}, any> & { getInitialProps?(context: NextPageContext): any; }'
Enter fullscreen mode Exit fullscreen mode

Fix the first error

The first one we can easily fix, by importing a proper type for the page param:

import { ReactNode } from 'react';
Enter fullscreen mode Exit fullscreen mode

Let's use it in our code:

// pages/_app.tsx

export default function MyApp({ Component, pageProps }) {
  // Use the layout defined at the page level, if available
  const getLayout = Component.getLayout || ((page: ReactNode) => page)

  return getLayout(<Component {...pageProps} />)
}
Enter fullscreen mode Exit fullscreen mode

Great! The first error is gone.

Fix the second error

The second one is more complicated. What happens is that original type for Component doesn't include getLayout function. We need to declare a new types. Let's create a next.d.ts file somewhere in the project, with the following content:

// next.d.ts

import type {
  NextComponentType,
  NextPageContext,
  NextLayoutComponentType,
} from 'next';
import type { AppProps } from 'next/app';

declare module 'next' {
  type NextLayoutComponentType<P = {}> = NextComponentType<
    NextPageContext,
    any,
    P
  > & {
    getLayout?: (page: ReactNode) => ReactNode;
  };
}

declare module 'next/app' {
  type AppLayoutProps<P = {}> = AppProps & {
    Component: NextLayoutComponentType;
  };
}
Enter fullscreen mode Exit fullscreen mode

It creates new types NextLayoutComponentType and AppLayoutProps that we can use in place of original types. Our initial code will need to be changed to this:

// pages/_app.tsx

import { AppContext, AppInitialProps, AppLayoutProps } from 'next/app';
import type { NextComponentType } from 'next';
import { ReactNode } from 'react';

const MyApp: NextComponentType<AppContext, AppInitialProps, AppLayoutProps> = ({
  Component,
  pageProps,
}: AppLayoutProps) => {
  const getLayout = Component.getLayout || ((page: ReactNode) => page);
  return getLayout(<Component {...pageProps} />);
};

export default MyApp;
Enter fullscreen mode Exit fullscreen mode

Please note that we are using the custom type we've created - AppLayoutProps. It includes the other custom type for Component that now contains getLayout function.

This solution was based on ippo012/nextjs-starter project, in which author used a very similar approach.

Top comments (5)

Collapse
 
mauriciorobayo profile image
Mauricio Robayo • Edited

Nice! Just a few days ago I faced this issue and solved it by modifying _app.tsx in the following way:

import type { AppProps } from "next/app";
import { ReactNode } from "react";
import { NextPage } from "next";

type Page<P = {}> = NextPage<P> & {
  getLayout?: (page: ReactNode) => ReactNode;
};

type Props = AppProps & {
  Component: Page;
};

const App = ({ Component, pageProps }: Props) => {
  const getLayout = Component.getLayout ?? ((page: ReactNode) => page);
  return getLayout(<Component {...pageProps} />);
};
export default App;
Enter fullscreen mode Exit fullscreen mode

Any drawbacks to this approach against the one you propose?

Thanks for sharing!

Collapse
 
carloschida profile image
Carlos Chida

That's just brilliant! Thanks for sharing.

I see some generics missing though — at least from next@11.1.0. I added them and it ended up being this for me:

import type { NextPage } from 'next';
import type { AppProps } from 'next/app';
import Head from 'next/head';
import type { ReactNode } from 'react';
import React from 'react';

type GetLayout = (page: ReactNode) => ReactNode;

// eslint-disable-next-line @typescript-eslint/ban-types
type Page<P = {}, IP = P> = NextPage<P, IP> & {
  getLayout?: GetLayout;
};

// eslint-disable-next-line @typescript-eslint/ban-types
type MyAppProps<P = {}> = AppProps<P> & {
  Component: Page<P>;
};

const defaultGetLayout: GetLayout = (page: ReactNode): ReactNode => page;

function MyApp({ Component, pageProps }: MyAppProps): JSX.Element {
  const getLayout = Component.getLayout ?? defaultGetLayout;

  return (
    <>
      <Head>
        <title>My site</title>
      </Head>
      {/* eslint-disable-next-line react/jsx-props-no-spreading */}
      {getLayout(<Component {...pageProps} />)}
    </>
}

export default MyApp;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
leotaozeng profile image
Leo Zeng • Edited

I added some generics based on your code.

import { NextPage } from 'next';
import type { AppProps } from 'next/app';
import { ScriptProps } from 'next/script';
import 'styles/main.scss';

type Page<P = Record<string, never>> = NextPage<P> & {
  Layout: (page: ScriptProps) => JSX.Element;
};

type Props = AppProps & {
  Component: Page;
};

const Noop = ({ children }: ScriptProps) => <>{children}</>;

function App({ Component, pageProps }: Props) {
  const Layout = Component.Layout || Noop;

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

export default App;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
forinda profile image
Orinda Felix Ochieng

Amazing works. This is a very useful resource

Collapse
 
eliozashvili profile image
Giorgi Eliozashvili

This works for sure, but I'd like to have more explanation of How does this works. As a Junior developer it's hard to understand :D and I don't want to take information without knowing answer on the question ''Why"