dois:pontos

Criando um link com estado de ativo no Next.js

27 de setembro de 2021 - 7 min

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 tipo ReactNode ou JSX.Element for usado, funciona também, mas como teremos apenas um elemento como filho, o mais correto é usar o ReactElement.
  • 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!

Links


  1. Spread operator: Veja mais sobre ele no MDN.
  2. React.cloneElement: Veja mais na documentação do React.
  3. Operador de coalescência nula: Veja mais sobre ele no MDN.

Apoie este blog

Se este artigo te ajudou de alguma forma, considere fazer uma doação. Isto vai me ajudar a criar mais conteúdos como este!
Clique aqui!

Comentários

Elves Sousa © 2023