Link y useRouter/usePathname en Next.js App Router: navegación del lado del cliente explicada con ejemplos
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:
Linksigue siendo el componente recomendado para navegación declarativa.useRouterahora expone métodos comopush,replace,back,forwardyrefresh.usePathnamepermite 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 defectotrue) que indica si Next.js debe precargar la ruta en segundo plano cuando el enlace entra en el viewport.replace: si se establece atrue, 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 desactivadoEste 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 → /loginusePathname
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/42Buenas prácticas y errores comunes
Buenas prácticas
- Preferí
Linkpara 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. - Usá
replacesolo 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»). - Limité el uso de
useRoutera 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. - Combiná
usePathnameconuseEffectsólo cuando necesites ejecutar lógica secundaria (como obtener datos). Si solo necesitás leer la ruta para renderizado, basta con el hook directamente. - 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. - Recordá que los componentes que usan
useRouterousePathnamedeben 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. - Mantené la accesibilidad: aunque
Linkevita la recarga completa, sigue renderizando un<a>interno con elhrefcorrecto, 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.pushdentro del render sin envolverlo en un manejador de evento. Esto provoca una navegación en cada render, provocando bucles o redirecciones infinitas. - Confundir
useRouter.pathnameconusePathname. 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
replaceen 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
useRouteryusePathname, 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á!
DUGLAS MORENO
Comentarios (0)
Sé el primero en comentar.