Editor’s note: This article was last updated on 19 July 2023 to include information about how dynamic props are handled in Next.js 13.
Next.js is an excellent tool for building web applications with React. I see it as Ruby on Rails for React applications. One of the benefits of using Next.js to build React web apps is that it handles routing for you. Over the years, I have used a variety of routing libraries, including React Router, found, Navi, and now Next.js.
In the past, I often had to switch libraries or update React Router, which at every major version is like a new library. Because of this, I got into the habit of isolating routing from the rest of my application. In this article, I’m going to explain two of my techniques for isolating routing in your application. I use Next as an example, but the techniques can be applied to almost all routing libraries.
Jump ahead:
Link
component
Link
componentMy first technique for isolating routing in your app is by using the Link
component. Every routing library has a similar component that is used instead of the <a>
tag. When clicked, it changes the URL without a full page redirect, and then the routing handles loading and displaying the new page.
In almost all of my projects, I use my own component named Link
. This component wraps the underlying routing library’s Link
component. Next has a similar Link
component. Its interface is different than that of the others, but it functions in the same way:
<Link href="/about"> <a>About</a> </Link>
This Link
component is a bit cumbersome for my taste because it adds a lot of visual density to your pages. This alone can be a good reason to wrap a component. In this case, however, I have even bigger reasons.
Say I want to migrate out of Next to something like Gatsby. To do so, I’d have to change much of my code structure; it won’t just replace imports from next/link
to gatsby/link
. Here is how a wrapped version of Link
is going to work:
import * as React from 'react'; import Link from 'next/link'; // allow this component to accept all properties of "a" tag interface IProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> { to: string; // we can add more properties we need from next/link in the future } // Forward Refs, is useful export default React.forwardRef(({ to, ...props }: IProps, ref: any) => { return ( <Link href={to} passHref> <a {...props} ref={ref} /> </Link> ); }); Note: I'm using TypeScript for all examples. Code will work without types as well.
This is how it will be used:
<Link to="/about">About</Link>
The new Link
component starts simple, but over time, you can add more functionality. A good candidate for additions is overwriting the defaults for the library.
In Next 9, automatic prefetching was turned on by default, and this remains the case in Next 13. This feature prefetches link contents when they are in the page’s viewport. Next.js uses a browser API called IntersectionObserver
to detect this. While automatic prefetching can be a useful feature, it can also be overkill if you have many links and dynamic pages. It is okay to use for the static side. Usually, I want to use automatic prefetching for specific pages, not for all of them. Or you might want to prefetch only when the mouse is hovering over the link.
Our Link
component makes it simple to turn automatic prefetching off:
interface IProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> { to: string; prefetch?: boolean; } export default React.forwardRef(({ to, prefetch, ...props }: IProps, ref: any) => { return ( <Link href={to} passHref prefetch={prefetch || false}> <a {...props} ref={ref} /> </Link> ); });
Now imagine if we didn’t have our Link
component and we had to turn off prefetching for every link.
Link
componentThe following are the benefits of isolating routing in your app is by using the Link
component:
Link
component’s functionality by adding additional props or features to the custom Link
componentLink
component makes it easy to test because we only need to mock a single component, so all components using this navigation won’t need to trigger an actual routeLink
for navigation, we can easily update the custom component without affecting other components that rely on itOne thing I see people doing in React applications is hardcoding links. This might look like the following:
<Link to="/about">About</Link> <Link to="/contact">Contact</Link>
This is very brittle because it’s not type-safe, and it makes renaming URLs or changing URL structure difficult.
The way I solve this is to have a file named path.ts
at the root of the project. It looks something like the following:
export default { about: '/about', contact: '/contact', }
This file contains all the routes in my application. This is how it is used:
import paths from '~/paths'; <Link to={paths.about}>About</Link> <Link to={paths.contact}>Contact</Link>
This way, I can change the routes, and I’m protected from typos whenever I need to use the path.
The following are the benefits of using a single file to hold all routing paths:
Before the release of version 9, Next didn’t support routes like /products/1
out of the box. You had to use an external package like next-router or URLs like /products?id=1
.
Dynamic routes are handled differently in Next.js 13. Now, we can pass a great number of props to Link
, but only one prop is required. Other props aren’t required because they have values they default to, such as the href
prop, which is the prop that the path or URL link navigates to.
replace
: Defaults to false. When true, next/link
will replace the current history state instead of adding a new URL into the browser’s history stackprefetch
: Defaults to true. When true, next/link
will prefetch the page (denoted by the href
) in the background. This is useful for improving the performance of client-side navigations. Any <Link />
in the viewport (initially or through scroll) will be preloaded. Prefetch can be disabled by passing prefetch={false}
. Prefetching is only enabled in productionpassHref
: Defaults to false. Forces Link
to send the href
property to its childscroll
: Defaults to true. Scroll to the top of the page after navigationshallow
: Defaults to false. Update the path of the current page without rerunning getStaticProps
, getServerSideProps,
or getInitialProps
locale
: Defaults to disabled. This allows you to provide a different locale. The active locale is automatically prepended. When not provided, href
has to include the localescroll={false}
: The default behavior associated with Link
is to scroll to the top of the page when there is a hash with an ID specified, like a <a>
tag. The scroll={false}
helps prevent this default behavior:
<Link href="/#top" scroll={false}> Won't scroll to the top</Link>
replace
: The default behavior associated with Link
is to add a new URL into the history
stack. The replace
prop can you use to prevent that. It replaces the current history state instead:
<Link href="/home" replace> Home </Link>
Let’s see how this addition of the dynamic route impacts our solution. Here is how it looks in practice:
<Link href="/products/[id]"> <a>Product 1</a> </Link>
This makes dealing with links even more cumbersome. Fortunately, we have our custom Link
and paths
. We can combine them and have the following:
<Link to={paths.product(product)}Product 1</Link>
How is this implemented? First, we add a function in paths
that returns both props for the page:
export default { about: '/about', contact: '/contact', // paths can be functions // this also makes it easier to change from "id" to "slug" in the future product(product: { id: string }) { return '/products/[id]' } }
Then, we have to handle the props:
interface IProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> { // allow both static and dynamic routes to: string ; prefetch?: boolean; } export default React.forwardRef(({ to, prefetch, ...props }: IProps, ref: any) => { // when we just have a normal url we jsut use it return ( <Link href={to} passHref prefetch={prefetch || false}> <a {...props} ref={ref} /> </Link> ); });
In this article, we explored how to deal with links in a Next.js app using two simple techniques that have helped me over the years. The techniques help you decouple your application from underlying libraries, make your system easy to change, and have type safety.
I hope you also find them useful. For any questions or comments, you can ping me on Twitter.
Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your Next.js apps — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
2 Replies to "Dealing with links in Next.js"
Hello, im getting 404 when refreshing page to which i’ve navigated using Link with as, i.e. simple any ideas? Using next 9.4.4
It looks like you have a typo in your product path function:
“`js
product(product: { id: string }) {
return {
href: ‘/products/[id],
as: `/products/${id}`,
};
}
“`
You are missing the closing quote for href. I think it should be:
“`
product(product: { id: string }) {
return {
href: ‘/products/[id]’,
as: `/products/${id}`,
};
}
“`
But I’m a PHP guy, so please correct me if I’m wrong.