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.
//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.
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
- products
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.
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).
.active {
color: red;
}
.active:after {
content: ' (current page)';
}
You should see something like this...
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.
Top comments (0)