Chris.luChris.lu header image, displaying an 80s style landscape and sunset

Navigation and next/link

In this chapter, we will build a basic navigation to navigate between two pages: the home(page) and a second page

So first, let's create that second page

In the app folder, create a new blog folder

Then, inside the blog folder, create a new page.tsx with the following content:

/app/blog/page.tsx
export default function Blog() {
 
    return (
        <>
            I&apos;m the blog page
        </>
    )
}

Next, we are going to create a component for the main Navigation bar of our app

In the components folder (that is in the root of the project), create a new header folder

Then, inside of the header folder, create a new Navigation.tsx file and add the following content:

/components/header/Navigation.tsx
import type { Route } from 'next'
 
interface IMainMenuItem {
    pathname: string
    text: string
}
 
const HeaderNavigation: React.FC = () => {
 
    const mainMenuItems: IMainMenuItem[] = [
        { pathname: '/', text: 'Home' },
        { pathname: '/blog', text: 'Blog' },
    ]
 
    return (
        <>
            <nav id="navigation">
                {mainMenuItems.map((menuItem) => {
                    return (
                        <a
                            href={menuItem.pathname as Route}
                            key={menuItem.pathname}
                            title={menuItem.text}
                        >
                            {menuItem.text}
                        </a>
                    )
                })}
            </nav>
        </>
    )
}
 
export default HeaderNavigation

First, we create a typescript interface and then use it to type our static list of pages. Of course, in a more complex project, you might want to fetch this list from a database (at build time), but for our simple example, a json will do, then we iterate through the list to create an entry for each page in our nav element

The important part here is to use the Route type (that we imported on top) for the href value to ensure that next/link understands it is a route and not just a random string, without the Route type, you will end up seeing errors like this one:

Next.js Typescript Type error: Type string is not assignable to type UrlObject | RouteImpl

Finally, we open the layout.tsx that is in the app folder to import/use the navigation component, like so:

/app/layout.tsx
import './global.css'
import { Metadata } from 'next'
import HeaderNavigation from '@/components/header/Navigation'
 
export const metadata: Metadata = {
    title: 'Next.js',
    description: 'Generated by Next.js',
}
 
export default function RootLayout({
    children,
}: {
    children: React.ReactNode
}) {
    return (
        <html lang="en">
            <body>
                <header>
                    <HeaderNavigation />
                    <p>{new Date().toString()}</p>
                </header>
                <main>{children}</main>
                <footer>
                    <p>My Footer</p>
                </footer>
            </body>
        </html>
    )
}

Line 3: we import our new HeaderNavigation component

Line 19: we replace the "my header" placeholder paragraph we had earlier with our new HeaderNavigation component

Line 20: we display the current date & time, which we will use for a test in a bit

For now our links use a regular HTML anchor element, this is to demonstrate the difference between anchor elements and using next/link (which we will use in the next chapter)

Now save the layout file and launch the dev server if it isn't already running (using the npm run dev command)

Next, open your browser and go to the project’s homepage at http://localhost:3000/

Then open the developer tools (by pressing the F12 key or right-clicking inside of the page and selecting Inspect) and make sure the Network Tab is open

Make sure the network tab is empty by clicking on the Clear networking log icon in the top left corner of the developer tools, to clear the networking log:

chrome developer tools clear networking log icon

Then click on the Blog link in the top navigation to go to the second page we just created

Now look at the Network tab logs, and you should see something similar to this:

chrome developer tools clear networking when using the anchor element

The most important information is the first row of the logs, if you look at the Type column, you will notice that the Blog page is a document, and in the Method column it will tell you that it was a GET request (I will explain in a bit why we did this initial test)

Now have a look at the time we print on top of the navigation, click on the navigation links, and notice how every time we navigate to a page, the time gets updated, which makes sense as we reload the entire page every time we navigate (we will see in the next chapter how this is different when using next/link)

Now let's replace the anchor element (<a>) we used in our navigation component with next/link

/components/header/Navigation.tsx
import type { Route } from 'next'
import Link from 'next/link'
 
interface IMainMenuItem {
    pathname: string
    text: string
}
 
const HeaderNavigation: React.FC = () => {
 
    const mainMenuItems: IMainMenuItem[] = [
        { pathname: '/', text: 'Home' },
        { pathname: '/blog', text: 'Blog' },
    ]
 
    return (
        <>
            <nav id="navigation">
                {mainMenuItems.map((menuItem) => {
                    return (
                        <Link
                            href={menuItem.pathname as Route}
                            key={menuItem.pathname}
                            title={menuItem.text}
                        >
                            {menuItem.text}
                        </Link>
                    )
                })}
            </nav>
        </>
    )
}
 
export default HeaderNavigation

Line 2: we import the Link component from the next/link package

Lines 21 and 27: we replace the HTML anchor element with the Link component

Now we repeat the steps we did in the previous chapter: ensure dev is running, open Chrome, navigate to http://localhost:3000/, open the developer tools, click on the Network tab, and clear the networking logs

Warning

Before you make the second test, make sure you are on the homepage (http://localhost:3000/) and that the Networking log is cleared

Now click again on the Blog link in the top navigation

Now look at the logs of the Networking tab once again:

chrome developer tools clear networking when using the next link

Have a look at the first line (actually also the second line because there are now two RSC fetches being made); remember last time when we used the anchor, we had a page request on the first line requesting the entire Blog document, this time we just have two fetch requests getting some RSC (react server component) data from the server; the total of requests is now 2 compared to the previous test where we had a total of 7; this is good because less requests means faster loading times, and less traffic from your server also means reduced hosting costs

Faster loading times are, of course, important because the user will be able to navigate faster, and if an app is fast, the user is happy, but it is also great because your server running Next.js will spend less time working on responses, which means it will be able to handle more requests and finally faster loading times will also improve your SEO score

Finally, look again at the time we print on top of the navigation, and you will notice that this time, it doesn't get updated anymore, this is because our layout does not get re-rendered every time we navigate to a new page, which is because Next.js has only replaced the main section of the page but did not make any changes to the layout (this is why on my blog, in the top header if you hit play; the songs will continue playing even if you visit a new page; 20 years ago to achieve the same thing I had to put the player into an iFrame, but today with Next.js / React this has become effortless)

Note

To be clear, server component layouts DO NOT re-render on navigation but client components (when using the 'use client' directive in a layout) DO re-render

You can now remove the <p>{new Date().toString()}</p> test (in /app/layout.tsx at line 20), which we added to demonstrate that the navigation does NOT re-render, we will not need it anymore

Highlight the current route using the next/navigation

Next, we are going to use the pathname of the current page, using the usePathname hook from next/navigation, then we will check if the first segment of the current path matches one of the entries in our navigation items list, if there is a match we will highlight that anchor element using CSS

Update the Navigation.tsx like so:

/components/header/Navigation.tsx
'use client'
 
import { usePathname } from 'next/navigation'
import type { Route } from 'next'
import Link from 'next/link'
 
interface IMainMenuItem {
    pathname: string
    text: string
}
 
const HeaderNavigation: React.FC = () => {
 
    const currentPagePathname = usePathname()
 
    const mainMenuItems: IMainMenuItem[] = [
        { pathname: '/', text: 'Home' },
        { pathname: '/blog', text: 'Blog' },
    ]
 
    const isActiveCheck = (menuItemPathname: string) => {
 
        let isActiveClass = ''
 
        if (menuItemPathname.length > 1) {
            if (currentPagePathname.startsWith(menuItemPathname)) {
                isActiveClass = 'active'
            }
        } else {
            // exception for homepage path which is "/"
            // in that case the page path needs to be equal
            if (currentPagePathname === menuItemPathname) {
                isActiveClass = 'active'
            }
        }
 
        return isActiveClass
    }
 
    return (
        <>
            <nav id="navigation">
                {mainMenuItems.map((menuItem) => {
                    return (
                        <Link
                            href={menuItem.pathname as Route}
                            key={menuItem.pathname}
                            className={isActiveCheck(menuItem.pathname)}
                            title={menuItem.text}
                        >
                            {menuItem.text}
                        </Link>
                    )
                })}
            </nav>
        </>
    )
}
 
export default HeaderNavigation

Line 1: we add the use client directive because we are going to use usePathname, which is a hook that we import at line 3; the component needs to be a client component when you use a hook as described in the Next.js "When to use Server and Client Components?" table; we only convert the component to a client component now because it is recommended to turn as few server components into client components; it is recommended to NOT convert all components to client components, only do so if a component needs to be interactive or requires things like the hook we are about to use

Line 3: we import usePathname from next/navigation

Line 14: we get the pathname for the current page

Lines 21 to 38: we have added a function that will check if the pathname starts with the path of one of the items in our navigation, if it does, then we add an active class to the className of that item

Line 48: we use our isActiveCheck function to check if that item is active, if it is, the function will add a class to className, else it will leave className empty

Now launch the dev server if it isn't already running (using the npm run dev command), then open your browser and go to the project’s homepage at http://localhost:3000/

Then right click into the page and select Inspect

Have a look at the the two links inside of the <nav> element (inside of the <header> element), you will see that one of the two now has an .active class and when you click on the links in the navigation that class will jump from one anchor to the other, depending on what page you are. In the next part of the tutorial we will add styling to make this active class more useful.

Congratulations 🎉 you created your navigation component using next/link and next/navigation

If you liked this post, please consider making a donation ❤️ as it will help me create more content and keep it free for everyone