Codementor Events

Removing “href” from Next.js Dynamic Routes

Published Nov 30, 2020
Removing “href” from Next.js Dynamic Routes

Note: This code is now redundant since Next.js announced Automatic Resolving of "href". However, this is still a great source for education and in some rare cases may still be useful - such as if a codebase is stuck on an older version of Next!

If you’ve used Next.js for any length of time, you know that pages and page routes are defined by the file structure of the app. This can simplify the need to manually declare routes and makes it easier to quickly get a simple app up and running!
However, this creates a pain-point when it’s time to work with lots of dynamic routes.

As a note, this post assumes you’re already familiar with how Next handles dynamic routes within it’s file structure. If not, you can always check out the documentation to learn about the specifics.

The main point of contention comes from how you have to define a route twice when you want to navigate to a page that’s generated by a dynamic route. If you decide to use either Next’s Link element or the very handy useRouter hook, you’ll have to define the link like so:

import Link from 'next/link';

<Link
  as="/blog/first-post"
  href="/blog/[post]"
>
  <a>First Post</a>
</Link>

As you can see, we had to write the URL string in as, but we also had to define the file-structure path in href. In most cases this is fine because typically you’ll know where you’re sending a user and you can just write the href line without a problem. However, this becomes a concern when you need to store a dynamic URL in a cookie or URL parameter because you’ll have to store both the URL and the file-structure path. Depending on your code structure and how you store the values, it may even be a potential security concern as the file-structure line will reveal your routing logic! Not to mention that this style of routing is extra cognitive overheard, especially when you are dealing with nested dynamic routes and catch-all routes in every link you write.

The solution is to eliminate the need to manually define href on every link by automatically deriving it based on the provided URL. The issue is that you can’t directly derive a file-structure path from the URL string! We’ll need to implement another tool. Thus comes path-to-regexp, a tool that allows us to convert path-strings to a regular expression and then check if a provided URL string matches the regexp!

Let’s pause for a moment and clarify some terminology we’re using. We’re now working with 2 different types of strings that relate to the logical mapping of URL routes.

  • file-structure path is the non-standard URL routing method used by Next.js to map a URL to their file-based routes.
    Some examples are: /blog/[post], /user/[id], /search/[[...filters]].
  • path-string is the standard URL routing method used in many websites, servers, etc. The same examples as above written in path-strings looks like this: /blog/:post, /user/:id, /search/:filters*.

There’s a problem with using the path-to-regexp library. As mentioned above, Next’s file-structure paths aren’t standard, and we’ll have to add an extra step that converts Next file-structure strings into a standard path-string. The simplest way to do this is creating a file of routes with all of our file-structure routes defined along with their path-string counterparts. The path-to-regexp github page has some basic instructions on writing standard path-strings. In short, if you’ve ever used react-router then you’ve already worked with path-strings.
Here’s an example routes file:

// routes.js

export default [  
  // Static Routes
  ['/', '/'],
  ['/blog', '/blog'],
   
  // Dynamic Route
  ['/blog/[post]', '/blog/:post'],
  
  // Catch-all Route
  ['/delete-posts/[...posts]', '/delete-posts/:posts+'],
   
  // Optional Catch-all Route 
  ['/search/[[...filters]]', '/search/:filters*']
];

Here we’ve declared an array that contains all of our links in our application. Each item is another array where the first value is Next’s file-structure path, and the second value is it’s path-string counterpart. With these defined we can easily match back-and-forth between the two!

It should be noted that the ordering of elements matters! If a URL would potentially match multiple items, we’re going to select the first one that it matches. You may also notice that our routes file isn’t very DRY. More on that later.

Next we need to create a function that will take a URL, check if it matches any of our listed string-paths, and then return the corresponding file-structure string. Don’t worry, it’s not as complicated as it sounds:

// deriveNextPath.js

import { match } from 'path-to-regexp';
import routes from '/routes';

const deriveNextPath = (url) => {

  // Remove any query params or hash mark from the URL for this process:
  const simplifiedUrl = url.split('?')[0].split('#')[0];
  let selectedfilePath = undefined;

  // Loop through each available route until we find the first match:
  routes.some((route) => {
    const [nextRoute, pathString] = route;
    
    // Convert the current path-string into a regular expression:
    const regexpMethod = match(pathString, { decode: decodeURIComponent });
    
    // Check if the regular expression matches the supplied URL:
    if (regexpMethod(simplifiedUrl)) {
      selectedfilePath = nextRoute;
      return true;
    }
  });

  // Return our final Next.js file structure path!
  return selectedfilePath;
};

export default deriveNextPath;

Technically, we’re done! All that’s left is to use our new deriveNextPath function anywhere we want to automatically generate our href. A great example is to create a wrapper for Link and then use it:

// MyLink.js

import Link from 'next/link';
import { deriveNextPath } from '/deriveNextPath';

const MyLink = (props) => {
  const {as, ...remainingProps} = props;
  
  return (
    <Link 
      as={as}
      href={deriveNextPath(as)}
      {...remainingProps}
    </Link>
  );
};

export default MyLink;


---------------------------------------
// Using MyLink.js
  
import MyLink from '/MyLink';

<MyLink as="/blog/first-post">
  <a>First Post</a>
</MyLink>

The href value will be automatically generated for us as long as our routes.js file stays up to date.

Want to try a few optimization challenges?

  • As I mentioned above, the routes.js file isn’t DRY for static routes. What change needs to be made to allow you to declare static routes only once within that file?
  • In almost all cases in our routes.js file, we’ll declare dynamic/catch-all/etc routes to match between file-structure paths and path-strings in the exact same way, so how can we take this code a step further and automatically generate our path-strings based on the file-structure paths?
Discover and read more posts from Josiah Dudzik
get started