Written on

Routes and Layouts in NextJS

Learning (and breaking) NextJS's layouts


Today I completed Chapter 4 of the NextJS tutorial. However, I was in the mood of learning by doing some freeform experimenting. So, before starting the tutorial, I decided to write some nested routing on my own, in the attempt to break things (spoiler alert: it didn't take long for me to break things!).

Some messy experiments

I started by creaing a simple about page copying and pasting the layout from the index page.

/app/about/page.tsx

export default function About() {
    function getDate() {
        return new Date().toLocaleDateString();
    }
    return (
        <>
            <h1>Welcome! Today is { getDate() }</h1>
            <h2>Here are some links to get you started:</h2>
        </>
    )
}

/app/about/layout.tsx

import '@/app/ui/global.css';
import { lusitana } from '@/app/ui/fonts';


export default function RootLayout({
    children,
}: {
    children: React.ReactNode;
}) {
    return (
        <html lang="en">
            <body 
                className={`${lusitana.className} antialiased`}
            >
                {children}
            </body>
        </html>
    );
}

The error here is Prop className did not match. Server: "__className_e66fe9 antialiased" Client: "__className_712214 antialiased

On a first rendering of the page sometimes visualized the font correctly. But then this error is thrown:

Warning: Prop className did not match. Server: "__className_e66fe9 antialiased" Client: "__className_712214 antialiased "

Perhaps changing the function name to match it with the node name would help?

import '@/app/ui/global.css';
import { lusitana } from '@/app/ui/fonts';


export default function AboutLayout({
    children,
}: {
    children: React.ReactNode;
}) {
    return (
        <html lang="en">
            <body 
                className={`${lusitana.className} antialiased `}
            >
                {children}
            </body>
        </html>
    );
}

Nope, same issue:

Warning: Prop className did not match. Server: "__className_e66fe9 antialiased" Client: "__className_712214 antialiased "

I tried to export just the <h1> tag to see if that would solve the problem. While the font renders correctly, now we have a hydration problem.

import '@/app/ui/global.css';
import { lusitana } from '@/app/ui/fonts';


export default function AboutLayout({
    children,
}: {
    children: React.ReactNode;
}) {
    return (

        <>
            <h1 className={`${lusitana.className} antialiased `}>
                {children}
            </h1>
        </>

    );
}

Hydration failed because the initial UI does not match what was rendered on the server. Warning: Expected server HTML to contain a matching <h1> in <h1>. See more info here: https://nextjs.org/docs/messages/react-hydration-error

Since the original page contained a <h2> element, I thought that perhaps removing it would help avoiding this issue. However, even without the <h2> element, the same error persists.

In the end, I tried to fix the error by wrapping the text in a <div> element instead of a <h1> one. This time, the page rendered without a problem.

/app/about/page.tsx

import '@/app/ui/global.css';
import { lusitana } from '@/app/ui/fonts';


export default function AboutLayout({
    children,
}: {
    children: React.ReactNode;
}) {
    return (
        <>
            <div className={`${lusitana.className} antialiased `}>
                {children}
            </div>
            {/* No errors */}
        </>
    );
}

/app/about/layout.tsx

export default function About() {
    function getDate() {
        return new Date().toLocaleDateString();
    }
    return (
        <>
            <div>
                Welcome! Today is {getDate()}
            </div>
            {/* No errors */}
        </>
    )
}

The problem was clearly about how HTML tag nesting is handled here. Enough hands-on experimenting: time to do some learning through the tutorial!

Routes and Layouts in NextJS

Here's a quick rundown of Chapter 4 of the NextJS tutorial. Each subfolder and its page.tsx file constitute a URL path on the app (which in NextJS jargon is called a route).[^10]

For example:

/app/about/page.tsx --> domain.com/about/

The default function of each page.tsx can be technically named anything really, not necessarily the name of the folder node. For example, in an /app/about/page.tsx we can have:

export default function About() {
// works
}
export default function Page() {
// also works
}

I'm wondering if there is a naming convention for exported default functions (other than just being clearly named, of course!)

Layouts

You can also include a layout.tsx to help in creating the UI of the page.

/app/about/layout.tsx --> domain.com/about/ /app/about/page.tsx --> domain.com/about/

/app/about/page.tsx and /app/about/page.tsx are rendered as https://domain.com/about/.

However, the real power and purpose of layout.tsx is that it applies to all pages in the same node.

/app/about/layout.tsx --> domain.com/about/, domain.com/about/contacts /app/about/page.tsx --> domain.com/about/ /app/about/contacts/page.tsx --> domain.com/about/contacts

/app/about/layout.tsx is rendered as https://domain.com/about/ and https://domain.com/about/contacts.

To summarize:

Also, /app/layout.tsx is a mandatory part of any NextJS project, since it is shared by all pages in the project. That's where you actually modify the <html> and <body> of the app.

Correcting the experiment

Going back to the experiment, I realize that in the tutorial project the about page is not rendering correctly because the <h1> tags are positioned inside other HTML elements. But if I wrap <h1>{children}</h1> inside of <article>, then things work fine

/app/about/page.tsx

export default function About() {
    function getDate() {
        return new Date().toLocaleDateString();
    }

    return (
        <>
            Welcome! Today is a beautiful day
            {/* No errors */}
        </>
    )
}

/app/about/layout.tsx

import '@/app/ui/global.css';
import { lusitana } from '@/app/ui/fonts';


export default function AboutLayout({
    children,
}: {
    children: React.ReactNode;
}) {
    return (
        <>
            <article>
                <h1>{children}</h1>
            </article>
            {/* No errors */}
        </>

    );
}

Result:

A browser showing a page and the element inspector matching what the code was supposed to do.

However, based on what I read in the tutorial, this would clearly a poor use of layout.tsx. Perhaps a more productive use of this tool would be to create a heading with a date that applies to all posts of the same route.

/app/about/layout.tsx

import '@/app/ui/global.css';
import { lusitana } from '@/app/ui/fonts';


export default function AboutLayout({
    children,
}: {
    children: React.ReactNode;
}) {
    function getDate() {
        return new Date().toLocaleDateString();
    }
    function GetTitle() {
        return (
            <h1>Daily post of { getDate() }</h1>
        );
    }
    return (
        <>
            <article>
                <GetTitle />
                <div className={`${lusitana.className} antialiased `}>
                    {children}
                </div>
            </article>
            {/* No errors */}
        </>
    );
}

/app/about/page.tsx

export default function About() {
    return (
        <>
            <article>
                <div>
                    Welcome! Today is a beautiful day
                </div>
            </article>
        </>
    )
}

Result:

Alt text

Of course that is just a simple implementation of a layout, but I can't wait to use it for more complex tasks!