5 Next.js Routing Best Practices
Jasser Mark Arioste
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.
To fix this we can either do one of the following:
- Wrap ExpensiveComponent in React.memo
- Since only the
<button>
uses the router, create another component for the<button>
so that theuseRouter
will be placed there. This removes Navbar's dependency to theuseRouter
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