Creating a link with an active state in Next.js
Tags: webdev, nextjs, javascript, tutorial
Published: 2021-10-01 20:52:19

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.

```tsx
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.

```tsx
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.

```tsx
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:

```tsx
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:

```tsx
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`:

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

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

```tsx
...

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

...
```

Inside the component, an argument is then added to the function with the _spread operator_[^1], so that all the native properties of the Next.js link can be accessed and passed on to the returned component in the function.

```tsx
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.

```tsx
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:

```tsx
<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:

```tsx
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 (thanks Lyrod for your insights). 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`:

```tsx
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"_.

```tsx
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:

```tsx
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:

```tsx
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

- [See our ActiveLink working](https://next-intl.elvessousa.com.br)
- [This article in Portuguese](https://blog.elvessousa.com.br/post/linkativo-next)
- [React Router DOM](https://www.vim.org)
- [Gatsby](https://www.gatsbyjs.com)
- [next/link](https://nextjs.org/docs/api-reference/next/link)

[^1]: **Spread operator**: Read more about it at [MDN](https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Operators/Spread_syntax).
[^2]: **React.cloneElement**: See more at [React docs](https://reactjs.org/docs/react-api.html#cloneelement)
[^3]: **Nullish coalescing operator**: Read more about it [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator).

---

If this article helped you in some way, consider [donating](https://www.buymeacoffee.com/elvessousa). This will help me to create more content like this!