Radoslav Stankov Software developer. Head of Engineering at Product Hunt. Organizer of @ReactNotAConf. Speaker. Trying to blog more at blog.rstankov.com 🤖

Dealing with links in Next.js

4 min read 1322

Dealing With Links In Next.js

Next.js is an excellent tool for building web applications with React. I kinda see it as Ruby on Rails for React applications. It packs a lot of goodies.

One of those goodies is that it handles routing for you.

However, over the years, I have used various routing libraries — a couple versions of react-router, found, Navi, and now Next.

Often, I 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 they can be applied to pretty much all routing libraries:

  • Use a custom Link component
  • Have all paths in a single file

Technique 1: Custom Link component

My first technique is wrapping the Link component. Every routing library has a similar component; it 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 Link component.

Next has a similar Link component. Its interface is a bit different than that of the others, but it functions in the same way:

<Link href="/about">
  <a>About</a>
</Link>

I understand why they designed it this way. It is quite clever; it uses React.cloneElement internally. You can check its code here. However, it is a bit cumbersome for my taste. It adds a lot of visual destiny to your pages.

This alone can be a good enough 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. I’d have to change a lot of 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}>
      <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 quite 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. This prefetches link contents when they are in the page’s viewport. Next uses a new browser API called IntersectionObserver to detect this.

This is a handy feature, but it can be overkill if you have a lot of links and pages that are dynamic. It is OK for the static side. Usually, I want to have this for specific pages, not for all of them. Or you might want to prefetch only when the mouse is hovering the link.

Our Link component makes it simple to turn this feature 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} 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.

Technique 2: Have all paths in a single file

One thing I see people doing in React applications is hardcoding links. Something like the following:

<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>

This is very brittle. It is not type-safe, and it makes renaming URLs or changing URL structure hard.

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>

In this way, I can change the routes, and I’m protected from typos.



Handling dynamic routes in Next

Next 9 was an epic release. Its most significant feature was support for dynamic route segments.

Before that, Next didn’t support dynamic routes like /products/1 out of the box. You had to use an external package like next-router or use URLs like /products?id=1.

The way dynamic routes are handled, we need to pass two props to Link:

  • href: Which file is this in the pages folder
  • as: How this page is shown in address bar

This is necessary because the Next client-side router is quite light and doesn’t know about the structure of your whole route. This scales quite well since you don’t hold complicated routing structures in browser memory, as in other routing systems.

Here is how it looks in practice:

<Link href="/products/[id]" as="/product/1">
  <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 {
      href: '/products/[id],
      as: `/products/${id}`,
    };
  }
}

Second, we have to handle those props:

interface IProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
  // allow both static and dynamic routes
  to: string | { href: string, as: string };
  prefetch?: boolean;
}

export default React.forwardRef(({ to, prefetch, ...props }: IProps, ref: any) => {
  // when we just have a normal url we jsut use it
  if (typeof to === 'string') {
    return (
      <Link href={to} prefetch={prefetch || false}>
        <a {...props} ref={ref} />
      </Link>
    );
  }

  // otherwise pass both "href" / "as"
  return (
    <Link href={to.href} as={to.as} prefetch={prefetch || false}>
      <a {...props} ref={ref} />
    </Link>
  );
});

Migration story

Before version 9, Next didn’t support dynamic routing. This was a big issue, and I had been using next-router for dynamic routing. It has a central file where you create the mapping from URL to file in the pages folder. Its Link component works quite differently.

It was a lifesaver before Next 9. But when dynamic routes were added to Next, it was time to stop using the library; it is even in maintenance mode now.

Imagine having a large application with hundreds of links. How much time do you think a migration like this could have taken?

For me, it took less than an hour. I just replaced the code in the Link component and changed the dynamic paths to return an object and not a route/params as next-router wanted.

Conclusion

Those techniques have helped me a lot over the years working with React applications. They are relatively simple but help you to decouple your application from underlying libraries, make your system easy to change, and have type safety.

I hope you find them useful as well. For any questions or comments, you can ping me on Twitter.

LogRocket: Full visibility into production Next.js apps

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 — .

Radoslav Stankov Software developer. Head of Engineering at Product Hunt. Organizer of @ReactNotAConf. Speaker. Trying to blog more at blog.rstankov.com 🤖

2 Replies to “Dealing with links in Next.js”

  1. 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

  2. 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.

Leave a Reply