How to Target and Style the Active Link in Next.Js (With Typescript)
Tags: nextjs, react, typescript, javascript
Published: 2022-08-23 14:51:00

Most React projects use React Router's `activeClassName` to target active routes. But building a navigation component in Next.JS with a styled active link isn't as straightforward.

In Next.js, the built-in `<Link>` component requires customization to achieve a similar effect.

Let's explore two solutions using Typescript: a basic one and a detailed (recommended) one.

## Basic Solution

This is an example of a basic solution which uses a custom `ActiveLink` component and the `useRouter` hook.

```ts
//Set up your navigation component with a custom 'ActiveLink' component (imported) from a separate file.
// Then create a page route file and component for each 'href' path i.e. index.tsx, about.tsx, products.tsx 

import ActiveLink from './ActiveLink';

const Nav = () => {
  return (
    <nav>
      <ul className="nav">
        <li>
          <ActiveLink href="/">
            Home
          </ActiveLink>
        </li>
        <li>
          <ActiveLink href="/about">
            About
          </ActiveLink>
        </li>
        <li>
          <ActiveLink
            href="/products/"
          >
            Products
          </ActiveLink>
        </li>     
      </ul>
    </nav>
  );
};

export default Nav;


```

**Next, let's build the `ActiveLink` component to recreate active link behavior.**

```ts
import { useRouter } from 'next/router'
import { LinkProps } from 'next/link';

//LinkProps is a type that requires 'href' as a prop. We're extending it to include a react element as a children prop.
type ActiveLinkProps = LinkProps & {
  children: ReactElement;
}

// href is the url path passed as a prop in the Nav component. The children are the string names passed in-between the ActiveLink tags.
function ActiveLink({ children, href }: ActiveLinkProps) {

// Deconstruct `asPath` from the router object to access the current page path shown in your browser (including the search params).
  const {asPath} = useRouter()
  
  //define the styling for the active link. If the current page path matches the 'href' path provided, display a red link. All other links will be black.
  const style = {
    color: asPath === href ? 'red' : 'black',
  }

  // Navigate to the page provided as 'href' when the link is clicked (router.push is used for client-side transitions)
  const handleClick = (e) => {
    e.preventDefault()
    router.push(href)
  }

  //the active link will have a style of 'color:red' 
  return (
    <a href={href} onClick={handleClick} style={style}>
      {children}
    </a>
  )
}

export default ActiveLink
```


This is a decent solution. But what if we want to scale our app to include server-side rendering, dynamic routes, custom Link props, and much more?

Here are some more tweaks to our `ActiveLink` component:

## Recommended Solution

**First**, in your `Nav` component add a `activeClassName` prop with an `active` string to the `ActiveLink` component of each page route.

You can also add a dynamic "catch-all" route for nesting pages within /products i.e. /products/categories. Make sure to create corresponding page routes in the pages folder like so:

- pages 
   - products 
       + [...slug]   // default page for all "catch-all" routes
       + index.tsx  //default home page for /products

```ts

import ActiveLink from './ActiveLink';

const Nav = () => {
  return (
    <nav>
      <ul className="nav">
        <li>
          <ActiveLink activeClassName="active" href="/">
            <a>Home</a>
          </ActiveLink>
        </li>
        .....
        //add the 'activeClassName' to each ActiveLink as shown in the previous section.
       ...... 
       
       // this is an example of a dynamic route using query paramaters.
        <li>
          <ActiveLink
            activeClassName="active"
            href="/products/[...slug]"
            as="/products/categories?limit=5"
          >
            <a>Products Categories </a>
          </ActiveLink>
        </li>
      </ul>
    </nav>
  );
};

export default Nav;

```

**Second**, let's revamp our ActiveLink component to take into account the `activeClassName` prop and additional props you may pass in the future. 

We also need to ensure that `asPath` from the `useRouter` hook doesn't lead to a mismatch of routes if the page is rendered using server-side rendering.

To avoid this, Next.js docs recommends the use of `isReady`: a boolean used to check whether the router fields are updated on the client-side.

```ts
import { useRouter } from 'next/router';
import Link, { LinkProps } from 'next/link';
import React, { useState, useEffect, ReactElement, Children } from 'react';

//Add the activeClassName as a required prop
type ActiveLinkProps = LinkProps & {
  children: ReactElement;
  activeClassName: string;
};

const ActiveLink = ({
  children,
  activeClassName,
  ...props
}: ActiveLinkProps) => {

  //deconstruct 'isReady' from the useRouter hook.
  const { asPath, isReady } = useRouter();
  
  //create an empty string as the default className of the component
  const [className, setClassName] = useState('');

  useEffect(() => {
    // isReady checks if the router fields are updated client-side (it must be used inside a useEffect hook)
    if (isReady) {
    
      // URL().pathname will help to eliminate query and hash strings from the url. 
      // Props.as targets dynamic routes, whilst props.href targets normal static page routes.
     
      const linkPathname = new URL(
        (props.as || props.href) as string,
        location.href
      ).pathname;
      
      // Here we make use of 'asPath' in the correct context (once 'isReady' is true)
      const activePathname = new URL(asPath, location.href).pathname;
      
      
      // Attach the activeClassName to the matching current page 
      const newClassName =
        linkPathname === activePathname
          ? `${activeClassName}`: '';

      // Sets a new 'className' state if there is a mismatch between the current and previous state. This ensures a 'toggle' like behavior between link changes.
      if (newClassName !== className) {
        setClassName(newClassName);
      }
    }
// useEffect dependencies defined below
  }, [
    asPath,
    isReady,
    props.as,
    props.href,
    activeClassName,
    setClassName,
    className,
  ]);

  return (
     // return the in-built Next Link including a child (a clone the 'a' element (child) including the activeClassName if it is the active page)
    <Link {...props}>
      {React.cloneElement(child, {
        className: className || null,
      })}
    </Link>
  );
};

export default ActiveLink;



```

**Finally**, add styling to `.active` in the global css stylesheet (typically imported into `_app` tsx).

```css

.active {
  color: red;
}

.active:after {
  content: ' (current page)';
}

```
You should see something like this...
![Next Js Active Link](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fi8p8c0mr2nh72hvf4mn.PNG)

## Recap

A simple solution to target and style an active link in Next.Js is to create a custom Link component which utlizes the `useRouter` hook to access the current path and returns a Link component with an `activeClassName`.

This `activeClassName` can be styled via css to display the active link of the page route.