Creating Active Links with Next.js

date
Jan 1, 2021
published
slug
creating-active-links-with-nextjs
description
How do you make a link display that it is the current page with Next.js? Make a custom component that handles the logic for you!
Recently I started to use Tailwind UI, which is truly excellent. While the examples are powered by Alpine JS, the code that is actually given is without any JavaScript code or opinion in how you implement it. Instead you get comment blocks that explain the intent.
<!-- Current: "text-gray-500", Default: "text-gray-400 group-hover:text-gray-500" -->
This was from a sidebar navigation snippet. In this example, they are trying to describe what classes should be present to communicate to the user which item in the menu reflects the current page.
If I was using React Router, there is a built in way to handle this. But using Next.js, the next/router and next/link has no concept that the page you are on is the current one. Instead you have to handle this yourself.
By creating your own Link component, you can wrap the Next.js Link component to make it a little smarter. Not only do we want to tell the component what classes should apply for the current page, but also what should apply when we are not the current page. Additionally, we may need to do this for children as well.
In the example below, the a tag needs to be adjusted based on if it is the active link. And the sag needs to be adjusted so the icon has the correct coloring as well, otherwise it would break the theme.
<nav class="px-2 space-y-1">
	<!-- Current: "bg-gray-100 text-gray-900", Default: "text-gray-600 hover:bg-gray-50 hover:text-gray-900" -->
  <a href="#" class="bg-gray-100 text-gray-900 group flex items-center px-2 py-2 text-base font-medium rounded-md">
    <!-- Current: "text-gray-500", Default: "text-gray-400 group-hover:text-gray-500" -->
    <!-- Heroicon name: home -->
    <svg class="text-gray-500 mr-4 h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
    </svg>
    Dashboard
	</a>
  ...
</nav>
We can achieve this by creating a ActiveLink.jsx component that takes the default and current class string. We will also append a Child component to the ActiveLink so that we can share the state of active with the child.
import React, { Children } from "react";
import { useRouter } from "next/router";
import Link from "next/link";

let parentActive = false;

const processChildren = (
  active,
  children,
  defaultClassName,
  currentClassName
) => {
  const child = Children.only(children);
  const childClassName = child.props.className || "";
  const className = active
    ? `${currentClassName} ${childClassName}`.trim()
    : `${defaultClassName} ${childClassName}`.trim();

  return React.cloneElement(child, {
    className: className || null,
  });
};

export default function ActiveLink({
  children,
  default: defaultClassNames,
  current: currentClassNames,
  ...props
}) {
  const { asPath } = useRouter();
  const active = Boolean(asPath === props.href || asPath === props.as);
  parentActive = active;

  return (
    <Link {...props}>
      {processChildren(active, children, defaultClassNames, currentClassNames)}
    </Link>
  );
}

const Child = function ActiveLinkChild({
  children,
  default: defaultClassNames,
  current: currentClassNames,
  ...props
}) {
  return (
    <>
      {processChildren(
        parentActive,
        children,
        defaultClassNames,
        currentClassNames
      )}
    </>
  );
};

ActiveLink.Child = Child;
We can now rewrite this html and apply the correct information using our custom ActiveLink component. Now when we are on the current page, the ActiveLink component can identify this because we checked our current page against the next/router. We then know if we need to add any classes.
The last piece is that we use ActiveLink.Child to give this same behavior to a child farther down the tree. This time it does not have to check against the next/router, and instead uses the parentActive` Boolean to determine how it should behave.
<nav className="px-2 space-y-1">
	<ActiveLink
		href="#"
		current="bg-gray-100 text-gray-900"
		default="text-gray-600 hover:bg-gray-50 hover:text-gray-900"
	>
	  <a href="#" className="group flex items-center px-2 py-2 text-base font-medium rounded-md">
			<ActiveLink.Child
				current="text-gray-500"
				default="text-gray-400 group-hover:text-gray-500"
			>
		    <svg className="mr-4 h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
		      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
		    </svg>
			</ActiveLink.Child>
	    Dashboard
	  </a>
	</ActiveLink>
  ...
</nav>