ReactHustle

5 Next.js Routing Best Practices

Jasser Mark Arioste

Jasser Mark Arioste

5 Next.js Routing Best Practices

Today I'm going to share with you next.js routing 5 best practices. These will be useful for a production grade application and try optimize the performance score on lighthouse. This article assumes that you have a basic understanding of next.js routing. This article touches on a few points that will sure come in handy! 

useRouter hook is a pretty straight forward react hook in next.js but sometimes it can cause performance issues and unwanted behavior when we fail to notice minor details when implementing a component.  

Be careful when using useRouter in a container component #

Placing useRouter inside a large/container component may result in unnecessary re-renders. This doesn't only apply to useRouter but also other hooks that uses React.Context  .  Lets examine the code below: 

//Navbar.ts

import Link from "next/link";
import { useRouter } from "next/router";
import React, { memo } from "react";
import ExpensiveComponent from "../home/ExpensiveComponent";

const Navbar = () => {
  const router = useRouter();
  console.log("Rendering Navbar");
  const links = ["home", "about", "contact"];
  return (
    <div>
      <ExpensiveComponent />
      {links.map((link) => {
        return (
          <Link href={"/" + link} key={link}>
            <a style={{ marginRight: 8 }}>{link}</a>
          </Link>
        );
      })}
      <button
        onClick={() => {
          router.push("/otherpath");
        }}
      >
        Some functionality
      </button>
    </div>
  );
};

export default memo(Navbar);
123456789101112131415161718192021222324252627282930313233

So in the code above, useRouter is used in a Navbar component. This component has another component inside that is expensive to render represented by ExpensiveComponent. In addition, the router is only used by the button to push a route (this is oversimplified but it happens pretty frequently). Now ideally, we want this navbar to render as few times as possible. Any change in the route will re-render the Navbar along with it's children.

Navbar is re-rendered with every change in route.

To fix this we can either do one of the following:

  1. Wrap ExpensiveComponent in React.memo
  2. Since only the <button> uses the router, create another component for the <button> so that the useRouter will be placed there. This removes Navbar's dependency to the useRouter hook.

We'll choose solution number 2, because please take note that there's also a cost on using React.memo. Now the code looks like this:

//Navbar.tsx
import Link from "next/link";
import React, { memo } from "react";
import ExpensiveComponent from "../home/ExpensiveComponent";
import { SomeButton } from "./SomeButton";

const Navbar = () => {
  console.log("Rendering Navbar");

  const links = ["home", "about", "contact"];
  return (
    <div>
      <ExpensiveComponent />
      {links.map((link) => {
        return (
          <Link href={"/" + link} key={link}>
            <a style={{ marginRight: 8 }}>{link}</a>
          </Link>
        );
      })}
      <SomeButton />
    </div>
  );
};

export default memo(Navbar);
1234567891011121314151617181920212223242526
//SomeButton.tsx

import { useRouter } from "next/router";
import React from "react";

export const SomeButton = () => {
  const router = useRouter();
  return (
    <button
      onClick={() => {
        router.push("/otherpath");
      }}
    >
      Some functionality
    </button>
  );
};
1234567891011121314151617

Since Navbar doesn't depend on useRouter anymore, It will only re-render if it's parent component re-renders. All it's children, including ExpensiveComponent will only render once even if the route path changes. Awesome!

Use prefetch={false} to reduce the number of requests on page load #

Let's say a page has a huge number of links and although next.js only prefetches the links when scrolled in viewport, Sometimes it still affects performance. Remember that we want as few requests as possible on first load. So to achieve it's a good practice to create a common Link component with prefetch={false}.

As a best practice, it's best to create a wrapper component to not forget about adding prefetch=false:

//LazyLink.tsx

import Link, { LinkProps } from "next/link";
import React from "react";

type Props = {
  children: React.ReactNode;
} & LinkProps;

const LazyLink = ({ children, ...props }: Props) => {
  return (
    <Link {...props} prefetch={false}>
      {children}
    </Link>
  );
};

export default LazyLink;
123456789101112131415161718

Now we can use it in another component:

//AnotherComponent.tsx
...
function AnotherComponent = () => {
  return <LazyLink href={"/about"} key={link}>
    <a style={{ marginRight: 8 }}>{link}</a>
  </LazyLink>
}

export default AnotherComponent;
123456789

Know when to use router.query properties #

Did you know that the next/link component can accept query and other properties as well? This is especially useful for pagination components and when there are a lot of query parameters in the URL. Let say your route is to following:
"/books?page=1&sort_by=title&sort_order=desc"

It would be extremely cumbersome to parse that query parameter if we wanted to change the page. It would be better to use router.query properties instead.

import { useRouter } from "next/router";
import React from "react";

const Pagination = () => {
  const router = useRouter();
  const pages = [1, 2, 3, 4];
  const handlePageChange = (page: number) => {
    router.push({
      query: {
        ...router.query,
        page,
      },
    });
  };
  return (
    <>
      {pages.map((page) => {
        <button onClick={() => handlePageChange(page)}>{page}</button>;
      })}
    </>
  );
};

export default Pagination;
123456789101112131415161718192021222324

In the code above, when clicking to a new page, we use the spread operator to only change the query part of the whole URL. This an extremely useful trick for complicated routes since we don't want to craft a template string just to change the page.

Use a loader Component when routing to a page using getServerSideProps #

Next.js is technically a Single-page application so every route change using next/router or next/link is fetched on the client-side. If a page using getServerSideProps takes time to load, it would seem that the page is "hanging" since it fetches the page through the network. one way to solve this is to show a "loading" status  component to indicate to the user that the page transitioning to the other one.

Know when to use next/link vs router.push for SEO #

Lets examine this Pagination component below. Even though we can change pages, it's not SEO optimized!

import { useRouter } from "next/router";
import React from "react";

const Pagination = () => {
  const router = useRouter();
  const pages = [1, 2, 3, 4];
  const handlePageChange = (page: number) => {
    router.push({
      query: {
        ...router.query, //preserve other query parameters here.
        page,
      },
    });
  };
 //button is not crawlable and search engine will not identify this as link
  return (
    <>
      {pages.map((page) => {
        <button onClick={() => handlePageChange(page)}>{page}</button>;
      })}
    </>
  );
};

export default Pagination;
12345678910111213141516171819202122232425

Search engines will not identify this as a link even though it's function links to other pages. So in this case it's better to use the next/link component with query parameters. We can use the spread operator to preserve other query parameters of the router. 

import { useRouter } from "next/router";
import React from "react";
import Link from "next/link"; 

const Pagination = () => {
  const router = useRouter();
  const pages = [1, 2, 3, 4];
  const query = router.query;
  return (
    <>
      {pages.map((page) => {
        <Link
          href={{
            query: {
              ...query,
              page,
            },
          }}
        >
          <a>{page}</a>;
        </Link>;
      })}
    </>
  );
};

export default Pagination;
123456789101112131415161718192021222324252627

The refactored code above behaves exactly like the first one with the benefit of SEO.

Conclusion #

These are the 5 best practices that we must keep in mind when developing a next.js application. If you know of any other best practices that are not listed here, please let me know! 

Credits:
- Photo by Fotis Fotopoulos on Unsplash

Share this post!

Related Posts