A guide to Next.js layouts and nested layouts

mangelosanto

Matt Angelosanto

Posted on July 6, 2023

A guide to Next.js layouts and nested layouts

Written by Ibadehin Mojeed✏️

When building projects with Next.js, we typically create an entire user interface by assembling isolated components.

However, some parts of the interface require the same code snippets across multiple routes — for example, the navigation header, footer, and sidebar. To manage this, we use layouts to structure the interface in a way that contains shared code snippets.

In this lesson, we'll delve into managing layouts and nested layouts in Next.js using the Pages Router and the App Router. We will cover:

Check out the demo project I've put together to see it in action: Next Js Nested Layouts Demo Project Showing Vercel Webpage With Three Menu Navigation Items, A Search Bar, And Various Page Elements You can see the project source code on GitHub as well. Here are the layout details for the project:

  • The Home and /dashboard/* routes share header and footer content
  • The Newsletter route has a different footer content and no header
  • The /dashboard/* routes implement a nested layout sharing sidebar navigation

To follow along with this tutorial, you’ll need a basic understanding of Next.js. Let’s get started!

Managing layouts with the Next.js Pages Router

Next.js recommends starting a new project with the App Router. However, in this tutorial, we'll also discuss how to implement layouts and nested layouts with the Pages Router for users who have yet to migrate to the new Next.js routing system.

To help illustrate the differences between the two approaches, we'll create the same application using both methods and compare how the new App Router simplifies the process of implementing nested layouts.

To start, let's take a look at a typical folder structure for the Pages Router in Next.js:



...
├── components
│    ├── Footer.js
│    └── Header.js
├── pages
│    ├── dashboard
│    │       ├── account.js
│    │       ├── analytics.js
│    │       └── settings.js
│   ...
│    ├── index.js
│    └── newsletter.js
...


Enter fullscreen mode Exit fullscreen mode

To define a layout with the Pages routing system, we create a Layout component that renders any shared user interface and its children.

Create a components/Layout.js file and render the shared header and footer content:



import Header from './Header';
import Footer from './Footer';

const RootLayout = ({ children }) => {
  return (
    <>
      <Header />
      <main>{children}</main>
      <Footer />
    </>
  );
};

export default RootLayout;


Enter fullscreen mode Exit fullscreen mode

The Layout component takes a children prop that serves as a placeholder for the active page content.

In the final project, we use Tailwind CSS for styling purposes. As a result, the updated markup includes class utilities:



const RootLayout = ({ children }) => {
  return (
    <div className="flex flex-col min-h-screen mx-auto max-w-2xl px-4 pt-8 pb-16">
      <div className="flex-grow">
        <Header />
        <main className="my-0 py-16">{children}</main>
      </div>
      <Footer />
    </div>
  );
};


Enter fullscreen mode Exit fullscreen mode

A beginner’s approach might be to wrap each page's rendered markup with the <RootLayout> component. For example, wrapping the Home component render would look like this:



import RootLayout from '@/components/Layout';

const Home = () => {
  return (
    <RootLayout>
      <main>{/* ... */}</main>
    </RootLayout>
  );
};
export default Home;


Enter fullscreen mode Exit fullscreen mode

By doing this, we'll get the desired UI for the pages: User Shown Typing Into Search Bar In Next Js Nested Layouts Demo Project. Text In Search Bar Disappears When User Navigates To A Different Page However, this implementation doesn't preserve the state between page transitions. For example, the search field's input text gets cleared when navigating between pages that share a common layout. This isn't the experience we expect from a single-page application.

In the next section, we'll discuss how to preserve the state in a shared layout.

Persisting layouts in the Next.js Pages Router

If we examine the pages/_app.js file that Next.js calls during each page initialization, we’ll see an App component that includes a Component prop representing the active page:



import '@/styles/globals.css'

export default function App({ Component, pageProps }) {
  return <Component {...pageProps} />
}


Enter fullscreen mode Exit fullscreen mode

In this file, we can load the shared layout and other global files, such as the global CSS file. So, let’s wrap the page content with the <RootLayout> like so:



import '@/styles/globals.css';
import RootLayout from '@/components/Layout';

export default function App({ Component, pageProps }) {
  return (
    <RootLayout>
      <Component {...pageProps} />
    </RootLayout>
  );
}


Enter fullscreen mode Exit fullscreen mode

With this implementation, the RootLayout component is reused between page transitions. As a result, the state in the shared component, such as the Header, will be preserved.

We no longer need to wrap each page's render with the <RootLayout> component.

After saving all files and revisiting the application, we can write in the search field and see that the state now persists between page changes, which is an improvement!

Note that you may need to restart the development server if it doesn't work as expected.

Creating nested layouts with the Next.js Pages Router

To create a nested shared layout, as demonstrated in the /dashboard/* pages, we need to nest a new layout that renders the sidebar navigation within the root layout.

However, with the current implementation, simply wrapping the active route within the root layout — as we did in the pages/_app.js file — only works if we require one layout for the entire application.

To achieve a nested layout, Next.js provides a way to compose layouts on a per-page basis.

Composing per-page layouts with the getLayout function

With the Next.js pages directory, we can create multiple layouts and nest them, or create a custom layout that applies to specific routes using a per-page implementation. That means, instead of rendering a root layout in the pages/_app.js file, we'll let each individual page component be in charge of its entire layout.

Let's begin with the Home page. We can achieve a per-page layout by applying a getLayout property on the page component:



import RootLayout from '@/components/Layout';

const Home = () => {
  return <main>{/* ... */}</main>;
};

Home.getLayout = (page) => {
  return <RootLayout>{page}</RootLayout>;
};

export default Home;


Enter fullscreen mode Exit fullscreen mode

We defined a function that takes the current page as a parameter and returns the desired UI for the index page. Note that we don’t have to use the name getLayout — it can be any name.

We can now invoke that function in the pages/_app.js file, and pass the current page as an argument. To achieve this, we'll modify the App component in the pages/_app.js as follows:



export default function App({ Component, pageProps }) {
  // If page layout is available, use it. Else return the page
  const getLayout = Component.getLayout || ((page) => page);
  return getLayout(<Component {...pageProps} />);
}


Enter fullscreen mode Exit fullscreen mode

When Next.js initializes a page, it checks if a per-page layout is defined in the page component using the getLayout function. If the layout is defined, it is used to render the page. Otherwise, the page is rendered as is.

After saving the file, the Home page should now render with the specified layout.

Creating a nested DashboardLayout

To create a nested layout for pages under the /dashboard/* route segments, we need to create a new layout file called components/DashboardLayout.js. This file should export a component that returns a shared UI for these pages and uses the children prop to render their respective content:



const DashboardLayout = ({ children }) => {
  return (
    <div className="flex gap-8">
      <aside className="flex-[2]">
        {/* Include shared UI here e.g. a sidebar */}
      </aside>
      <div className="bg-gray-100 flex-[8] p-4 rounded min-h-[300px]">
        {children}
      </div>
    </div>
  );
};

export default DashboardLayout;


Enter fullscreen mode Exit fullscreen mode

Now, in each of the /dashboard/* page files, we need to apply a getLayout property on the page component and return the desired layout tree.

For example, the /dashboard/account.js file will look like this:



import RootLayout from '@/components/Layout';
import DashboardLayout from '@/components/DashboardLayout';

const Account = () => {
  return <div>Account screen</div>;
};

Account.getLayout = (page) => (
  <RootLayout>
    <DashboardLayout>{page}</DashboardLayout>
  </RootLayout>
);

export default Account;


Enter fullscreen mode Exit fullscreen mode

Notice how the DashboardLayout is nested within the RootLayout.

If we apply the getLayout property to the other page components under the /dashboard/* route, we’ll also get the desired layout where the state persists between page transitions: User Shown Typing Into Search Bar In Next Js Nested Layouts Demo Project. Text In Search Bar Remains When User Navigates To A Different Page Check out the GitHub files for the other page components to double-check your work so far.

Creating a custom layout in the Next.js Pages Router

You may want to create a custom layout as we have done on the Newsletter page in the final project. That layout renders different footer content, with no navigation bar or sidebar.

We'll create a new layout file components/OnlyFooterLayout.js and returns the custom footer and children prop. The code for this component would look like this:



import NewsletterFooter from './NewsletterFooter';

const OnlyFooterLayout = ({ children }) => {
  return (
    <div className="flex flex-col min-h-screen mx-auto max-w-2xl px-4 pt-8 pb-16">
      <div className="flex-grow">
        <main className="my-0 py-16">{children}</main>
      </div>
      <NewsletterFooter />
    </div>
  );
};

export default OnlyFooterLayout;


Enter fullscreen mode Exit fullscreen mode

Next, we’ll create the components/NewsletterFooter.js file and render some custom footer content:



const NewsletterFooter = () => {
  return (
    <footer className="flex items-center justify-between">
      {/* ... */}
    </footer>
  );
};

export default NewsletterFooter;


Enter fullscreen mode Exit fullscreen mode

Finally, in the pages/newsletter.js file, we’ll apply a getLayout property on the page component, then return the desired UI for the Newsletter page:



import OnlyFooterLayout from '@/components/OnlyFooterLayout';

export const Newsletter = () => {
  return (
    // ...
  );
};

Newsletter.getLayout = (page) => (
  <OnlyFooterLayout>{page}</OnlyFooterLayout>
);

export default Newsletter;


Enter fullscreen mode Exit fullscreen mode

If we save all files, the page should now render with the custom layout.

See the project source code.

Managing layouts in the Next.js App Router

Next.js 13 introduced the App Router file system, which enables first-class support for layouts, nested routes, and nested layouts. Beginning with version 13.4, we are safe to use this new routing system in a production environment.

In light of our project routes, the app directory structure would look something like this:



...
├── app
│   ...
│    ├── dashboard
│    │       ├── account
│    │       │     └── page.js
│    │       ├── analytics
│    │       │     └── page.js
│    │       └── settings
│    │             └── page.js
│    ├── newsletter  
│    │       └── page.js
│    ├── layout.js
│    └── page.js    
├── components
│   ...
│   
...


Enter fullscreen mode Exit fullscreen mode

Each folder or nested folder in the app directory defines a route or nested route and requires a special page.js file to render its respective UI.

The layout.js file at the top level is the root layout that is shared across all pages in the application. This file is required and would have the following structure by default:



export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}


Enter fullscreen mode Exit fullscreen mode

Next.js uses this root layout to wrap the page content or any nested layouts that may be present during rendering.

Similar to what we did in the pages directory, we can also include the top-level shared components within this root layout like so:



import Header from '@/components/Header';
import Footer from '@/components/Footer';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <div className="flex flex-col min-h-screen mx-auto max-w-2xl px-4 pt-8 pb-16">
          <div className="flex-grow">
            <Header />
            <main className="my-0 py-16">{children}</main>
          </div>
          <Footer />
        </div>
      </body>
    </html>
  );
}


Enter fullscreen mode Exit fullscreen mode

This layout will persist across routes and maintain component state, as anticipated.

It is important to note that the app directory represents the root segment, so the app/page.js file will render the UI of the index page:



const Home = () => {
  return <main>{/* ... */}</main>;
};
export default Home;


Enter fullscreen mode Exit fullscreen mode

It's worth noting that components in the app directory are React Server Components by default, unlike those in the pages directory. As a result, we cannot use client-side hooks in these components.

Therefore, to address this issue, we have extracted the logic for the active menu class and the search functionality — which utilize the usePathname and useState client-side Hooks, respectively — from the Header component and placed them in their separate client components.

Creating nested layouts in the Next.js App Router

To create a nested shared layout, specifically for pages under the /dashboard/* route segments, all we need to do is add a layout.js file inside the dashboard folder to define the UI:



const DashboardLayout = ({ children }) => {
  return (
    <div className="flex gap-8">
      <aside className="flex-[2]">
        {/* Include shared UI here e.g. a sidebar */}
      </aside>
      <div className="bg-gray-100 flex-[8] p-4 rounded min-h-[300px]">
        {children}
      </div>
    </div>
  );
};

export default DashboardLayout;


Enter fullscreen mode Exit fullscreen mode

This layout will be nested within the root layout and will wrap all the pages in the /dashboard/* route segment, providing a more specific and targeted UI for those pages.

That’s all. As we can see, creating a nested layout in the App Router is incredibly easy and intuitive.

Creating a custom layout in the Next.js App Router

In order to design a personalized layout for the Newsletter page, we must isolate that specific route segment from the shared layouts. To accomplish this, we will employ route groups.

Creating route groups

Route groups are a way to group related routes. For our project, we’ll create two route groups:

  1. A custom route group containing the Newsletter route: The layout for this group will render a custom layout with no navigation bar or sidebar and a different footer
  2. A primary route group containing both the index route and the /dashboard/* routes, since they share the same root layout

Note that we can name the route group anything we want. It's only for organizational purposes.

To create a route group, we'll wrap the group name in parentheses. If we reorganize the app directory into two groups, we'll have the following structure:



...
├── app
│    ├── (primary)
│    │       ├── dashboard
│    │       │       ├── account
│    │       │       │     └── page.js
│    │       │       ├── analytics
│    │       │       │     └── page.js
│    │       │       ├── settings
│    │       │       │     └── page.js
│    │       │       └── layout.js         
│    │       ├── layout.js         
│    │       └── page.js          
│    ├── (custom)  
│    │       ├── newsletter
│    │       │        └── page.js
│    │       └── layout.js
│    └── layout.js    
│   ...
│   
...


Enter fullscreen mode Exit fullscreen mode

Each of the groups has its respective layout, allowing us to customize the UI as desired. The (primary)/layout.js now looks like so:



export default function MainLayout({ children }) {
  return (
    <div className="flex flex-col min-h-screen mx-auto max-w-2xl px-4 pt-8 pb-16">
      <div className="flex-grow">
        <Header />
        <main className="my-0 py-16">{children}</main>
      </div>
      <Footer />
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

Meanwhile, the (custom)/layout.js looks like so:



import NewsletterFooter from '@/components/NewsletterFooter';

export default function CustomLayout({ children }) {
  return (
    <div className="flex flex-col min-h-screen mx-auto max-w-2xl px-4 pt-8 pb-16">
      <div className="flex-grow">
        <main className="my-0 py-16">{children}</main>
      </div>
      <NewsletterFooter />
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

Finally, the top-level app/layout.js file should now include the <html> and <body> tags as follows:



export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}


Enter fullscreen mode Exit fullscreen mode

If we save all the files, each route should render as expected! See the full project source code on GitHub.

Conclusion

Understanding how layouts work in Next.js is crucial for building complex projects with the framework. In this guide, we've covered all the necessary steps to structure the rendered UI with shared content and use it across multiple routes.

We've discussed how to achieve layouts and nested layouts both in the Pages Router and the new App Router. Additionally, we've seen how to use route groups to create custom layouts for specific route segments.

If you found this guide helpful, we encourage you to share it with others. If you have any questions or contributions, feel free to leave a comment.

See the final project hosted on Vercel.

💖 💪 🙅 🚩
mangelosanto
Matt Angelosanto

Posted on July 6, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related