dois:pontos

Creating a link with an active state in Next.js

September 27, 2021 - 7 min

There is something that, at the moment I write these lines, still lacks in Next.js: a component <Link /> showing a different class while the page is being visited.

Why use the link if you can use normal anchors?

Before continuing, a small pause, to see why using <Link /> instead of an <a>.

Basically, every time you use a normal anchor, the page makes a full refresh. The <Link /> component changes this behavior by loading only what changes on the screen, avoiding unnecessary rendering and making the experience faster and smoother. This is just for internal links; for the external ones, the anchor is enough.

React and Gatsby projects

In a React (CRA) project, this already comes by default with the React Router DOM library: just import a component <Link /> that comes with it, and add the activeClassName attribute, informing a CSS class for the active state of that anchor.

import { Link } from "react-router-dom"

export function Nav() {
  return (
    <nav>
      <Link to="/" activeClassName="active">
        Home
      </Link>
      <Link to="/blog" activeClassName="active">
        Blog
      </Link>
      <Link to="/about" activeClassName="active">
        About
      </Link>
    </nav>
  )
}

In Gatsby, another framework for creating static pages in React, the same can be achieved through the Gatsby library.

import { Link } from "gatsby"

export function Nav() {
  return (
    <nav>
      <Link to="/" activeClassName="active">
        Home
      </Link>
      <Link to="/blog" activeClassName="active">
        Blog
      </Link>
      <Link to="/about" activeClassName="active">
        About
      </Link>
    </nav>
  )
}

However, in Next.js, for some reason I don't know yet, the implementation of the <Link /> component is quite different: a child element is required and there are no to and activeClassName properties.

import Link from "next/link"

export function Nav() {
  return (
    <nav>
      <Link href="/">
        <a>Home</a>
      </Link>
      <Link href="/blog">
        <a>Blog</a>
      </Link>
      <Link href="/about">
        <a>About</a>
      </Link>
    </nav>
  )
}

It is a good implementation, meets multiple needs, but still lacks support for a class for the active state, as seen in previous examples.

How to bring activeClassName support to Next.js

Let's now create the <ActiveLink />: a component which will have the active class support. Here, the code is in typescript, but if your project uses JavaScript, the code works as well: just remove the typing. The component has only the required code for this feature to work.

First, we create the basic structure:

import { useRouter } from "next/router"
import Link from "next/link"

export function ActiveLink() {
  const { asPath } = useRouter()

  return <Link>...</Link>
}

The "hook" function useRouter is imported from Next.js, so that our component has information for the current route. This hook has the asPath property, which informs the current path of the page.

After this, let's create the properties of our component:

import { ReactElement } from "react"
import { useRouter } from "next/router"
import Link, { LinkProps } from "next/link"

type ActiveLinkProps = {
  children: ReactElement
  activeClassName: string
}

export function ActiveLink({ children, activeClassName }: ActiveLinkProps) {
  const { asPath } = useRouter()

  return <Link>{children}</Link>
}

Here, I use the ActiveLinkProps type to inform the properties that the component will accept:

  • children: It is a ReactElement type, that is, accepts a single React element as a parameter. If a ReactNode or JSX.Element type is used, it works as well, but as we will only have one element as child, is better to ReactElement.
  • activeClassName: With the 'string' type, as a simple text is enough to enter the name of a valid CSS class.

The problem is that at this time, the component doesn't have access to the properties of a normal <Link />. To do this, you need to extend the ActiveLinkProps type. Without these properties, the component will not work as a real replacement to the Next.js default link. Thus, it is necessary to import the Linkprops definition that comes with next/link:

import Link, { LinkProps } from "next/link"

After this, we make ActiveLinkProps aware of LinkProps type properties.

...

type ActiveLinkProps = LinkProps & {
  children: ReactElement
  activeClassName: string
}

...

Inside the component, an argument is then added to the function with the spread operator1, so that all the native properties of the Next.js link can be accessed and passed on to the returned component in the function.

import { ReactElement } from "react"
import { useRouter } from "next/router"
import Link, { LinkProps } from "next/link"

type ActiveLinkProps = LinkProps & {
  children: ReactElement
  activeClassName: string
}

export function ActiveLink({
  children,
  activeClassName,
  ...rest
}: ActiveLinkProps) {
  const { asPath } = useRouter()

  // The "...rest" represents all properties coming from LinkProps
  return <Link {...rest}>...</Link>
}

Now just make a conditional that verifies if the current route is the same as the "href" of the component.

const className = asPath === rest.href ? activeClassName : ""

If true, the class informed in activeClassName will be used.

Applying className in children components

Next.js' default implementation of <Link /> doesn't accept a className property. This should be passed on to a child element, otherwise it will not work:

<Link href="/">
  <a className="meuLink">Home</a>
</Link>

Therefore, to pass the property the correct way, we need to use the React.cloneElement()2 method to clone the child element, and passing className to it.

The final code will look like this:

import { cloneElement, ReactElement } from "react"
import { useRouter } from "next/router"
import Link, { LinkProps } from "next/link"

type ActiveLinkProps = LinkProps & {
  children: ReactElement
  activeClassName: string
}

export function ActiveLink({
  children,
  activeClassName,
  ...rest
}: ActiveLinkProps) {
  const { asPath } = useRouter()
  const className = asPath === rest.href ? activeClassName : ""

  return <Link {...rest}>{cloneElement(children, { className })}</Link>
}

One more thing...

If you're not like me, maybe you noticed I forgot something: the className in the child element gets replaced by activeClassName when the route is active. In many cases it will work properly, but if you need to have two classes in the same element like "mylink active", then this will not be enough.

To solve this little issue, we need to get the current child element's className first. This can be achieved by using children.props.className. After that, we merge it with activeClassName:

const childClassName = children.props.className
const newClassName = `${childClassName} ${activeClassName}`

The code above will print an undefined if children.props.className is not present. The same will happen with activeClassName. To get rid of these, we use the nullish coalescing operator ??3 to save a couple of "ifs".

const childClassName = children.props.className ?? ""
const newClassName = `${childClassName} ${activeClassName ?? ""}`

Now we just have to update the conditional to include this newClassName variable I came up with:

const className = asPath === rest.href ? newClassName.trim() : ""

The trim() part will eliminate spaces left when one of the classes is not available.

So, the real final code looks like this now:

import { cloneElement, ReactElement } from "react"
import { useRouter } from "next/router"
import Link, { LinkProps } from "next/link"

type ActiveLinkProps = LinkProps & {
  children: ReactElement
  activeClassName: string
}

export function ActiveLink({
  children,
  activeClassName,
  ...rest
}: ActiveLinkProps) {
  const { asPath } = useRouter()
  const childClassName = children.props.className ?? ""
  const newClassName = `${childClassName} ${activeClassName ?? ""}`
  const className = asPath === rest.href ? newClassName.trim() : ""

  return <Link {...rest}>{cloneElement(children, { className })}</Link>
}

That's all folks!

Links


  1. Spread operator: Read more about it at MDN.
  2. React.cloneElement: See more at React docs
  3. Nullish coalescing operator: Read more about it MDN.

Apoie este blog

Se este artigo te ajudou de alguma forma, considere fazer uma doação. Isto vai me ajudar a criar mais conteúdos como este!
Clique aqui!

Comentários

Elves Sousa © 2023