Está aí algo que, no momento em que escrevo estas linhas, ainda não existe de forma nativa no Next.js: um componente <Link />
que mostra uma classe diferente ao estar na página a qual ele se refere.
Por que de usar o Link se posso usar uma âncora normal?
Antes de continuar, uma pequena pausa, para abordarmos o porquê de usar o <Link />
em vez do <a>
.
Basicamente, toda vez que se utiliza uma âncora normal, o <a>
, a página recarrega por inteiro. O componente <Link />
evita este comportamento ao carregar apenas o que se altera na tela e evitar renderizações desnecessárias, tornando a experiência mais rápida e suave. Observe que tratamos aqui de links internos, para os externos a âncora normal é mais que suficiente.
Em projetos React e Gatsby
Em um projeto com o React padrão, isto já vem por padrão com a biblioteca React Router DOM, bastando apenas importar um componente <Link />
que vem com ela, e adicionar o atributo activeClassName
, informando uma classe CSS para o estado ativo daquela âncora.
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>
)
}
No Gatsby, outro framework para a criação de páginas estáticas em React, o mesmo pode ser obtido através da biblioteca Gatsby Link, que já vem com ele.
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>
)
}
Porém, no Next.js, por algum motivo que desconheço ainda, a implementação do componente <Link />
é bem diferente: um elemento filho é exigido e não existem as propriedades to
e activeClassName
.
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>
)
}
É uma boa implementação, atende a diversas necessidades, porém falta ainda o suporte a uma classe para o estado ativo, como visto nos exemplos anteriores.
Como trazer o suporte ao activeClassName para o Next.js
Vamos agora criar o componente <ActiveLink />
que terá o suporte para a classe ativa que desejamos ter. Aqui o código está em TypeScript, mas se o seu projeto usa JavaScript, o código funciona da mesma maneira, basta apenas retirar as tipagens. O componente abaixo tem apenas o necessário para o recurso funcionar.
Primeiro criamos a estrutura básica:
import { useRouter } from "next/router"
import Link from "next/link"
export function ActiveLink() {
const { asPath } = useRouter()
return <Link>...</Link>
}
Importa-se a função "gancho" (hook) useRouter
do Next.js, para que o nosso componente tenha conhecimento da rota da aplicação. Este gancho possui a propriedade asPath
, que informa o caminho atual da página, que basta para deixar o componente ciente da rota atual.
Após isto, criamos as propriedades do nosso componente:
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>
}
Aqui utilizo o tipo ActiveLinkProps
para informar quais são as propriedades que o componente irá aceitar:
- children: é um tipo
ReactElement
, isto é, aceita um único elemento React como parâmetro. Se um tipoReactNode
ouJSX.Element
for usado, funciona também, mas como teremos apenas um elemento como filho, o mais correto é usar oReactElement
. - activeClassName: com o tipo
string
, pois basta um texto simples para informar o nome da classe CSS.
O problema é que, agora, o componente não tem acesso a propriedades de um <Link />
normal. Para fazer isto, é preciso estender o tipo ActiveLinkProps
. Sem essas propriedades, o componente não funcionará como uma real substituição ao Link padrão do Next. Para fazer isto, é necessário importar a definição LinkProps
que vem em next/link
:
import Link, { LinkProps } from "next/link"
Depois disto, informamos que o tipo ActiveLinkProps
recebe também as propriedades do tipo LinkProps
.
...
type ActiveLinkProps = LinkProps & {
children: ReactElement
activeClassName: string
}
...
No componente, adiciona-se então um argumento na função com o spread operator1, para que todas as propriedades nativas do Link do Next.js possam ser acessadas e repassadas ao componente retornado na função.
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()
// Aqui o ...rest representa todos as propriedades
// vindas do tipo LinkProps
return <Link {...rest}>...</Link>
}
Com a propriedades informadas, agora basta fazer uma condicional que verifica se a rota atual é a mesma informada no "href" do componente.
const className = asPath === rest.href ? activeClassName : ""
Em caso afirmativo, a classe informada no activeClassName
será usada.
Aplicando o className nos componentes filhos
A implementação do <Link />
no Next.js não aceita a propriedade className
diretamente no componente. Esta deve ser passada no elemento filho, caso contrário não irá funcionar:
<Link href="/">
<a className="meuLink">Home</a>
</Link>
Portanto, para repassar a propriedade da maneira correta, precisamos usar o método React.cloneElement()
2 para clonar o elemento filho, repassando a classe nova nele.
O código final ficará assim:
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>
}
Mais uma coisa...
Se você não é como eu, talvez tenha percebido que esqueci algo: o className
no elemento filho é substituído por activeClassName
quando a rota está ativa. Em muitos casos, ele funcionará corretamente, mas se você precisa ter duas classes no mesmo elemento, como "mylink active"
, isto não será suficiente.
Para resolver este pequeno problema, precisamos capturar o className
atual do elemento filho primeiro. Isto é possível acessando children.props.className
. Após isto, juntamos ele ao activeClassName
:
const childClassName = children.props.className
const newClassName = `${childClassName} ${activeClassName}`
O código acima vai resulta num undefined
se o children.props.className
não está presente. O mesmo ocorrerá com o activeClassName
. Para se livrar disso, usamos o operador de coalescência nula ??
3 para economizar alguns "ifs".
const childClassName = children.props.className ?? ""
const newClassName = `${childClassName} ${activeClassName ?? ""}`
Agora basta atualizar o condicional para incluir esta variável newClassName
que criei:
const className = asPath === rest.href ? newClassName.trim() : ""
A parte do trim()
vai eliminar os espaços deixados quando uma das classes não estiver disponível.
Então, o verdadeiro código final ficará assim:
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 newClassName = `${children.props.className} ${activeClassName}`
const className = asPath === rest.href ? newClassName : ""
return <Link {...rest}>{cloneElement(children, { className })}</Link>
}
Isso é tudo pessoal!