Cut Into The Jamstack book logo

Cut Into TheJamstack

Next.js Page Layouts and Dynamic Content

Mike Cavaliere

11/19/2021


Next.js page layouts are great for reusing page outer templates. Instead of having a whole page re-render when you want to switch to another page with the same look and feel, persistent layouts can allow us to re-render only the dynamic portions of the page, resulting in a faster page transition.

But what if we want to to change a small bit of the page layout based on what page we're on - like highlighting the current page in a navigation menu or breadcrumb? This isn't documented in Next.js docs, but it's easy to do.

(Note: You can see all of these techniques in action on this site, and in the codesandbox / stackblitz examples below).

What are Next.js page layouts?

A quick definition: a page layout in Next.js is a React component that contains elements that you want to reuse on multiple pages. Those elements are typically at the top and/or bottom of the pages.

For example, if you want a batch of pages to be centered, you might have a CenteredLayout component that will center any page it's applied to:

export const CenteredLayout = ({ children }) => (
  <div style={{ margin: '0 auto', maxWidth: 650 }}>
    {children}
  </div>
)

A common use case is to have a header and footer on every page.

export const CenteredLayout = ({ children }) => (
  <Header />
    <div style={{ margin: '0 auto', maxWidth: 650 }}>
      {children}
    </div>
  <Footer />
)

Note the format of a layout component: it simply takes a React children prop, and supplies the layout-specific elements before or after the children.

Persistent layouts

Now you might think that to use a layout in Next.js you would simply include it on the pages you want to use it on, but don't do it! This will have some unintended consequences.

//
// Don't do this!
//
export default function Page1() {
  return (
    <CenteredLayout>
      <h1>Page 1</h1>

      <Link href="/page2">Go to page 2</Link>
    </CenteredLayout>
  )
}

//
// Don't do this!
//
export default function Page2() {
  return (
    <CenteredLayout>
      <h1>Page 2</h1>

      <Link href="/page1">Go to page 1</Link>
    </CenteredLayout>
  )
}

What's the problem with this? As Adam Wathan talks about in his article, this will lead to unnecessary re-renders. When navigating from page 1 to page 2, the whole page will re-render, even though they're both using the same layout.

Those two <CenteredLayout> components are different instances, and so have different subtrees, which React will re-render separately. So we can solve this by making them the same exact instance.

Preventing unnecessary re-renders with static getLayout() functions

To remedy this, we add the paradigm of using a static getLayout() function attached to a page, and tell Next.js to look for this method on a page component when rendering.

First we need a custom _app.js to the pages directory, as per the docs, and have it check for a getLayout() function on the page component.

//
// pages/_app.js
//
function MyApp({ Component, pageProps }) {

  // If the component has a getLayout() function, use it. Otherwise just render the page as is.
  const getLayout = Component.getLayout || ((page) => page);

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

export default MyApp;

In the layout component we'll export a getLayout() function that wraps the passed-in children with the layout:

//
// layouts/CenteredLayout.js
//
export const CenteredLayout = ({ children }) => (
  <div style={{ maxWidth: 500, background: '#FA9F42', margin: '0 auto' }}>
    {children}
  </div>
);

export const getLayout = (page) => <CenteredLayout>{page}</CenteredLayout>;


Now we can set any page to use the same layout by assigning its static getLayout() property to be the layout's getLayout() function.

//
// DO this!
//
// pages/Page1.js
//
import { getLayout } from '../layouts/CenteredLayout'

export default function Page1() {
  return (
    <>
      <h1>Page 1</h1>

      <Link href="/page2">Go to page 2</Link>
    </>
  )
}

Page1.getLayout = getLayout;

//
// DO this!
//
// pages/Page2.js
//
import { getLayout } from '../layouts/CenteredLayout'

export default function Page2() {
  return (
    <>
      <h1>Page 2</h1>

      <Link href="/page1">Go to page 1</Link>
    </>
  )
}

Now when navigating, you won't see the layout component re-render. The example above is fairly trivial, but you'll see that it remains true for large layouts as well. Result: more snappy page navigation, which we love Next.js for. 👏🏼

Nested layouts

Not only can pages have layouts, but your layouts can have layouts. Simplified examples (no imports, minimal styles):

//
// layouts/SiteLayout.js
//
export const SiteLayout = ({ children }) => (
  <>
    <Header />
    {children}
    <Footer />
  </>
)

SiteLayout.getLayout = (page) => (<SiteLayout>{children}</SiteLayout>)

//
// layouts/CenteredLayout.js
//
export const CenteredLayout = ({ children }) => (
  <div style={{ margin: '0 auto', maxWidth: 650 }}>
    {children}
  </div>
)

export const getLayout = (page)  => (<CenteredLayout>{page}</CenteredLayout>)

CenteredLayout.getLayout = getLayout

//
// pages/nested.js
//
import { getLayout as getSiteLayout } from '../layouts/SiteLayout'
import { getLayout as getCenteredLayout } from '../layouts/CenteredLayout'

export default function CenteredPageWithHeaderAndFooter() {
  return <h1>This is a centered page with a header and a footer</h1>;
}

CenteredPageWithHeaderAndFooter.getLayout = (page) => {
  return getSiteLayout(getCenteredLayout(page));
};

Dynamic content in Shared Layouts

What if you want to have dynamic content in them? Let's say you have a header in which you want to display the name of the current page you're on. The header is in the main site layout, but needs a title prop passed to it. To achieve this we simply augment the getLayout() pattern we're using to accept props from the page we're passing in, and pass them along to the layout (which can in turn pass them along to our Header component).

We augment _app.js so that it passes any page props to getLayout():

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

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

Our layouts need to pass the page props along as well:

//
// layouts/CenteredLayout.js
//
import { getLayout as getSiteLayout } from './SiteLayout';

export const CenteredLayout = ({ children }) => (
  <div style={{ maxWidth: 500, margin: '0 auto' }}>
    {children}
  </div>
);

export const getLayout = (page, pageProps) =>
  getSiteLayout(<CenteredLayout>{page}</CenteredLayout>, pageProps);

//
// layouts/SiteLayout.js
//


export const SiteLayout = ({ children, title }) => {
  return (
    <div>
      <Header title={title} />
      {children}
      <Footer />
    </div>
  );
};

export const getLayout = (page, { title }) => {
  return <SiteLayout title={title}>{page}</SiteLayout>;
};


Our Header component accepts the title prop and does something with it.

export const Header = ({ title }) => (
  <div>
    <h3>Header</h3>

    <div>
      <Link href="/header-footer">Header/Footer Only</Link> <span>|</span>{' '}
      <Link href="/centered">Centered</Link> <span>|</span>{' '}
      <Link href="/">Index Page </Link>
    </div>

    <h3>Page title: {title}</h3>
  </div>
);


And of course, lastly our pages need to have a title prop. Since this is Next.js, we can opt to do this statically:

import { getLayout } from '../layouts/CenteredLayout';

export async function getStaticProps() {
  return {
    props: {
      title: 'Index Page',
    },
  };
}

export default function IndexPage() {
  return <h1>Index page</h1>;
}

IndexPage.getLayout = getLayout;

You can see this full example in action here on StackBlitz.

Supported by

eb-logo-gradient-black-textCreated with Sketch.

© 2022 Mike Cavaliere. All rights reserved

TwitterMediumLinkedIn