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 aReactNode
orJSX.Element
type is used, it works as well, but as we will only have one element as child, is better toReactElement
. - 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 (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
:
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
If this article helped you in some way, consider donating. This will help me to create more content like this!
Top comments (5)
Using cloneElement, you'll override the className that already exists on the element, right?
I think you need to retrieve the current className and merge with active
Exactly!
cloneElement()
does just that.Thank you for the feedback. The article is updated with the implementation. Now the component is merging the classes.
Uodate 2 : using interpolation to concat both className is not a good idea. If children.props.className is undefined, I think you'll have "undefined className" in the new className
That's what happens when you do things in a hurry, hahaah... You were right again. I made this change, what do you think? This was enough to get rid of the
undefined
.This is better!