Master DRY in Next.js with Layouts

Master DRY in Next.js with Layouts

Featured on Hashnode

This is my course on Next.js called Bite-Sized Next.js, where we learn core Next.js concepts in 5 minutes or less.

Welcome back! Now that you've learned the essentials of pages in Next.js, it’s time to delve deeper and explore some of the more advanced features concerning pages. While I am calling them advanced ideas, you will definitely need to know them to build a truly well-crafted Next.js app.

Layouts

Imagine if you needed to figure out if a user can access a particular page in your app, and you wrote this code:

let canAccessPage: boolean;

if (!user.isLoggedIn) {
    canAccessPage = false;
}

canAccessPage = user.roles.includes(requiredRole);

You're proud of your solution, and you should be! Then, every time you need to check access, you copy and paste those lines everywhere. That wouldn’t be so smart anymore, would it? Intuitively, you would define a function to encapsulate the logic:

function checkAccess(user: User, requiredRole: string): boolean {
    return user.isLoggedIn && user.roles.includes(requiredRole);
}

For some reason Don’t Repeat Yourself is exclusively discussed in the context of code. Meanwhile I see lots of people breaking this guidance when it comes to markup.

Fortunately Next.js has a DRY answer for HTML: layouts! Layouts allow you to neatly encapsulate your markup, just like the function definition above does for checking a user’s access.

Essentially, layouts are just like any other page component in Next.js, but they accept a special children argument. This is where your child page will be populated in the layout.

A simple example

For example, if you were building a dashboard, you could use a layout like this:

export default function DashboardLayout({
  children, // will be a page or nested layout
}: {
  children: React.ReactNode;
}) {
  return (
    <main>
      <h1>This is the layout</h1>
      <section>
        {/* Include shared UI here e.g. a header or sidebar */}
        <nav></nav>
        {children}
      </section>
    </main>
  );
}

Your child page could be something like this:

export default function MyDashboard() {
  return <h2>This is my dashboard</h2>
}

And this would be the final rendering:

Where should I use layouts?

Think about all of the elements that are the same across your app. These are all great candidates for incorporating into your layout! Navigation headers, sidebars, and footers are probably the most common components that developers put into layouts.

The Next.js website itself has a common header and footer across many pages. I’ve highlighted them in blue below:

Using multiple layouts

Just like pages, layouts can be nested inside each other. When Next.js traverses your app hierarchy, it will make a note of all the layouts it finds along the path to each page.tsx file. Then all of those layouts will be stacked together to produce your completed markup. The Next.js docs have a nice visual of this process.

Other stuff to know

There are a couple other important things to know about layouts:

  1. You must define a root layout directly under the app/ folder, and it must contain the html and body tags. Something like this would suffice:
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {/* Layout UI */}
        <main>{children}</main>
      </body>
    </html>
  )
}
  1. You can have a layout.tsx and a page.tsx in the same folder, and the page will inherit from the layout.

Templates

Templates are a special case of layouts. They are almost identical in terms of behavior, but the notable difference is that templates do not persist local state in between renderings. Think of templates as layouts with short-term memory problems. 🙂

export default function Template({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>
}

Use cases

Why would you want to use templates? There are certain situations where you want the page to render from scratch. Here are a couple scenarios:

  1. You created a cool animation that runs with useEffect when a page loads. You always want the animation to play, no matter how many times the user has already navigated there.

  2. You have a client-side payment form that leverages useState and collects credit card numbers. If the user navigates away, you want the form to be wiped clean.

How layouts and templates work together

With templates in the mix, understanding the order in which layouts, templates, and pages will render can be a little complicated. It goes like this:

  1. Start at the top-level source folder (i.e. app)

  2. Render the layout (if present), then the template (if present)

  3. Have we reached the page we are looking for?

    1. Yes —> render the page and finish

    2. No —> move to the next folder down the hierarchy and repeat step (2)

Try to keep it simple

Maybe you’re wondering: if I have a layout within a template, will that layout persist between renders? The answer is no! In Next.js, you need to be mindful of how you organize things, especially state-holding components. If you find yourself nesting layouts and templates too deeply, you may want to simplify your layout hierarchy.

In this aspect, layout inheritance mirrors OOP (class) based inheritance. It’s a useful feature up to the point where it becomes too complex to understand. That’s when the effort of refactoring becomes worth the cost.

Conclusion

In this lesson, we've explored the powerful concepts of layouts and templates, tools that help us adhere to the DRY principle not just in our code but also in our markup. Understanding and implementing layouts will give your app a more consistent look and feel and help you manage page hierarchies effectively.

Next, we will delve into advanced routing techniques, helping you to create more dynamic Next.js applications. Subscribe to get the next tutorial!