Link y useRouter/usePathname en Next.js App Router: navegación del lado del cliente explicada con ejemplos

DUGLAS MORENODUGLAS MORENO 👁 6

Introducción

Si estás construyendo una aplicación con Next.js 13+ y estás usando el App Router, seguramente te hayas encontrado con la necesidad de moverte entre páginas sin recargar todo el sitio. El framework ofrece dos herramientas principales para lograr esto:

  • El componente Link, que permite navegación declarativa.
  • Los hooks useRouter y usePathname, que dan control imperativo sobre la ruta y permiten leer la URL actual.

Este artículo está dirigido a desarrolladores que ya conocen los fundamentos de React y Next.js, pero quieren profundizar en cómo funciona la navegación cliente en el nuevo modelo de carpetas app/. Al finalizar, sabrás cuándo usar cada herramienta, qué opciones ofrecen y cómo evitar los errores más comunes.

¿Qué cambió con el App Router?

En el antiguo pages/ router, la navegación se gestionaba con next/link y el hook useRouter de next/router. Con el App Router, esas APIs se mudaron a next/navigation y el comportamiento se alineó más con las convenciones de los Server Components. Sin embargo, para interacciones del lado del cliente (como clicks en menús o cambios de estado que requieren redirección) seguimos necesitando usar APIs cliente.

Los puntos clave:

  • Link sigue siendo el componente recomendado para navegación declarativa.
  • useRouter ahora expone métodos como push, replace, back, forward y refresh.
  • usePathname permite leer la ruta actual sin suscribirse a cambios (ideal para efectos que dependen solo de la URL).

El componente Link

Uso básico

El componente Link se importa de next/link y se comporta como un <a> mejorado: previene la recarga completa de la página, ejecuta la carga del segmento de ruta correspondiente y opcionalmente hace prefetch de los recursos necesarios.

import Link from 'next/link';

export default function Menu() {
  return (
    <nav>
      <ul>
        <li><Link href=\`/\">Inicio</Link></li>
        <li><Link href=\`/about\">Sobre nosotros</Link></li>
        <li><Link href=\`/blog\">Blog</Link></li>
      </ul>
    </nav>
  );
}

Cuando el usuario hace clic en cualquiera de esos enlaces, Next.js intercepta el evento, carga el módulo de la página destino (si es un Server Component lo renderiza en el servidor y envía el HTML) y actualiza el historial del navegador sin una recarga completa.

Prefetch y propiedades útiles

Link acepta varias props que ajustan su comportamiento:

  • prefetch: booleano (por defecto true) que indica si Next.js debe precargar la ruta en segundo plano cuando el enlace entra en el viewport.
  • replace: si se establece a true, la navegación reemplaza la entrada actual del historial en lugar de apilar una nueva.
  • scroll: controla si se debe hacer scroll al principio de la página (true) o mantener la posición de scroll (false).
  • locale: para aplicaciones con i18n, fuerza una locale específica.

Ejemplo con replace y scroll={false}:

<Link href=\`/producto/[id]\` replace scroll={false}>
  Ver producto {id}
</Link>

Al hacer clic, la URL cambiará a /producto/123 pero no se agregará una nueva entrada al historial; además, la página no hará scroll al top, lo que es útil en carruseles o modales que se actualizan mediante ruta.

Ejemplo con salida esperada (simulado)

Para demostrar qué ocurre internamente, podemos crear una función que registre lo que haría Next.js al interceptar un clic. Este bloque es autocontenido y se puede ejecutar directamente en el playground.

function simulateLinkClick(href, { replace = false, scroll = true } = {}) {
  console.log(`▶️ Click en enlace: ${href}`);
  if (replace) console.log('🔁 Usando replace (no se apila en historial)');
  else console.log('📥 Apilando nueva entrada en historial');
  console.log(scroll ? '📍 Scroll al top activado' : '📍 Scroll al top desactivado');
}

// Ejemplos de uso
simulateLinkClick('/about');
simulateLinkClick('/producto/42', { replace: true, scroll: false });

Salida esperada en la consola:

▶️ Click en enlace: /about
📥 Apilando nueva entrada en historial
📍 Scroll al top activado
▶️ Click en enlace: /producto/42
🔁 Usando replace (no se apila en historial)
📍 Scroll al top desactivado

Este comportamiento muestra cómo Link delega la lógica de navegación al router interno de Next.js.

Los hooks de navegación: useRouter y usePathname

Cuando necesitas realizar una navegación imperativa (por ejemplo, después de enviar un formulario, tras una autenticación o en respuesta a un evento de WebSocket) recurres a los hooks exportados por next/navigation.

useRouter

El hook useRouter devuelve un objeto con los siguientes métodos:

  • push(url, options?): navega a una nueva URL, apilando entrada en el historial.
  • replace(url, options?): navega sustituyendo la entrada actual.
  • back(): retrocede una entrada en el historial.
  • forward(): avanza una entrada en el historial.
  • refresh(): vuelve a cargar la ruta actual (útil después de un mutation).

Además, expone la propiedad pathname (solo lectura) que refleja la ruta actual, pero no es reactiva; si necesitas reaccionar a cambios de ruta, combina usePathname con useEffect.

Ejemplo imperativo con useRouter

'use client';

import { useRouter } from 'next/navigation';
import { useState } from 'react';

export default function LoginForm() {
  const router = useRouter();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  async function handleSubmit(e) {
    e.preventDefault();
    // Simulación de llamada a API
    await fakeLogin(email, password);
    // Tras login exitoso, redirigimos al panel
    router.push('/dashboard');
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        placeholder=\'Email\'
        value={email}
        onChange={e => setEmail(e.target.value)}
      />
      <input
        type=\'password\'
        placeholder=\'Contraseña\'
        value={password}
        onChange={e => setPassword(e.target.value)}
      />
      <button type=\'submit\'>Ingresar</button>
    </form>
  );
}

// Función falsa solo para que el ejemplo sea autocontenido
function fakeLogin(email, password) {
  return new Promise(res => setTimeout(() => res({ ok: true }), 800));
}

Al enviar el formulario, se ejecuta router.push('/dashboard'). En una aplicación real, esto provocaría una navegación cliente sin recarga completa.

Simulación de salida (run:)

Para ver qué haría el hook, podemos crear una versión simplificada que imprima en consola las acciones:

let historial = ['/'];
function useRouterMock() {
  return {
    push: (url) => {
      historial.push(url);
      console.log(`➡️ push → ${url}`);
      console.log(`   Historial: ${historial.join(' → ')}`);
    },
    replace: (url) => {
      historial[historial.length - 1] = url;
      console.log(`🔁 replace → ${url}`);
      console.log(`   Historial: ${historial.join(' → ')}`);
    },
    back: () => {
      if (historial.length > 1) {
        historial.pop();
        console.log(`⬅️ back → ${historial[historial.length - 1]}`);
      }
    },
    forward: () => {
      // En este mock no implementamos forward avanzado
      console.log('↔️ forward (no implementado en mock)');
    },
    refresh: () => {
      console.log(`🔄 refresh → ${historial[historial.length - 1]}`);
    }
  };
}

const router = useRouterMock();
router.push('/login');
router.push('/dashboard');
router.replace('/home');
router.back();
router.refresh();

Salida esperada:

➡️ push → /login
   Historial: / → /login
➡️ push → /dashboard
   Historial: / → /login → /dashboard
🔁 replace → /home
   Historial: / → /login → /home
⬅️ back → /login
🔄 refresh → /login

usePathname

usePathname es un hook que devuelve la ruta actual como una cadena. Su principal ventaja sobre leer router.pathname es que es reactiva: cada vez que la URL cambia, el componente que lo llama se vuelve a renderizar automáticamente.

Esto lo hace ideal para:

  • Mostrar u ocultar elementos según la sección activa de un menú.
  • Ejecutar efectos que dependen exclusivamente de la ruta (por ejemplo, cargar datos específicos de una vista).
  • Aplicar clases CSS dinámicas basado en la ruta.

Ejemplo de menú resaltado

'use client';

import { usePathname } from 'next/navigation';
import Link from 'next/link';

export default function Navbar() {
const pathname = usePathname();

const isActive = (href) => pathname === href || pathname.startsWith(href + '/');

return (
<nav className='flex space-x-4'>
<Link
href='/'
className={{
'font-bold': isActive('/'),
'text-gray-600': !isActive('/')
}}
>
Inicio

<Link
href='/about'
className={{
'font-bold': isActive('/about'),
'text-gray-600': !isActive('/about')
}}
>
Sobre nosotros

<Link
href='/blog'
className={{
'font-bold': isActive('/blog'),
'text-gray-600': !isActive('/blog')
}}
>
Blog


);
}


Cuando el usuario navega a `/blog/post/1`, la variable `pathname` será `\'/blog/post/1\'`, `isActive('/blog')` devolverá `true` y el enlace al blog se verá en negrita.

#### Simulación de salida (run:)

```run:javascript
let currentPath = '/';
function usePathnameMock() {
  return () => currentPath;
}

function setPath(path) {
  currentPath = path;
  console.log(`🛣️ Ruta cambiada a: ${currentPath}`);
}

const getPathname = usePathnameMock();
console.log('Valor inicial:', getPathname());
setPath('/about');
console.log('Después de cambiar:', getPathname());
setPath('/blog/post/42');
console.log('Después de cambiar:', getPathname());

Salida esperada:

Valor inicial: /
🛣️ Ruta cambiada a: /about
Después de cambiar: /about
🛣️ Ruta cambiada a: /blog/post/42
Después de cambiar: /blog/post/42

Buenas prácticas y errores comunes

Buenas prácticas

  1. Preferí Link para navegación declarativa siempre que sea posible. Es más accesible (se comporta como un <a> nativo) y permite que los rastreadores sigan los enlaces sin JavaScript.
  2. Usá replace solo cuando realmente no quieras dejar una entrada en el historial (por ejemplo, tras un login o después de una acción que no debería poder deshacerse con el botón «atrás»).
  3. Limité el uso de useRouter a manejadores de eventos (submit de formularios, clicks en botones, respuestas de WebSockets). No lo llames en el cuerpo del componente sin una condición, ya que provocaría navegaciones en cada render.
  4. Combiná usePathname con useEffect sólo cuando necesites ejecutar lógica secundaria (como obtener datos). Si solo necesitás leer la ruta para renderizado, basta con el hook directamente.
  5. Aprovechá el prefetch automático de Link. Si tenés enlaces que aparecen frecuentemente en el viewport (como en un menú lateral), dejá el prefetch activado; si sabés que una ruta es poco probable, podés desactivarlo para ahorrar ancho de banda.
  6. Recordá que los componentes que usan useRouter o usePathname deben ser marcados como 'use client'. Si los colocás dentro de un Server Component sin esa directiva, Next.js lanzará un error en tiempo de desarrollo.
  7. Mantené la accesibilidad: aunque Link evita la recarga completa, sigue renderizando un <a> interno con el href correcto, por lo que los lectores de pantalla y la navegación con teclado funcionan como esperás.

Errores comunes

  • Olvidar 'use client' en componentes que usan los hooks de navegación. El error típico es: "Invalid hook call. Hooks can only be called inside of the body of a function component." La solución es agregar la directiva al inicio del archivo.
  • Usar router.push dentro del render sin envolverlo en un manejador de evento. Esto provoca una navegación en cada render, provocando bucles o redirecciones infinitas.
  • Confundir useRouter.pathname con usePathname. El primero no es reactivo; si lo usás para mostrar un estado que depende de la URL, la UI no se actualizará al cambiar de ruta.
  • Desactivar el prefetch en enlaces críticos (como el logo que lleva al home) pensando que ahorras rendimiento, cuando en realidad el prefetch es muy liviano y mejora la percepción de velocidad.
  • Usar replace en flujos de múltiples pasos (como un wizard) sin pensar que el usuario no podrá volver al paso anterior con el botón «atrás».

Conclusión

El App Router de Next.js nos brinda una forma potente y flexible de manejar la navegación del lado del cliente mediante dos pilares:

  • El componente Link, ideal para enlaces declarativos que benefician del prefetch y de la semántica de <a>.
  • Los hooks useRouter y usePathname, que nos dan control imperativo y reactividad cuando necesitamos responder a eventos o adaptar la UI a la URL actual.

Al comprender cuándo usar cada herramienta y seguir las buenas prácticas señaladas, podrás construir aplicaciones que se sientan rápidas, accesibles y predecibles. Recordá siempre marcar como 'use client' los componentes que usan los hooks de navegación, evitar llamadas imperativas en el render y aprovechar el prefetch para mejorar la percepción de rendimiento.

Con estos conceptos en mano, estás listo para implementar flujos de navegación sofisticados — desde menús dinámicos y formularios que redirigen tras el login, hasta paneles de control que actualizan su vista únicamente según la ruta — sin sacrificar la experiencia de usuario ni la calidad del código.

¡A navegá!

Comentarios (0)

Sé el primero en comentar.

Dejá tu comentario