Radoslav Stankov Software Developer. Head of engineering at @ProductHunt. 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.

We made a custom demo for .
No really. Click here to check it out.

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.

Plug: , a DVR for web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Radoslav Stankov Software Developer. Head of engineering at @ProductHunt. Organizer of @ReactNotAConf. Speaker. Trying to blog more at blog.rstankov.com 🤖

Leave a Reply