Linking y Navegación
NextJS proporciona dos formas principales de navegar entre páginas: el componente Link para navegación declarativa y el hook useRouter para navegación programática. Esta guía explica cómo usar ambos correctamente.
El componente Link
El componente Link es la forma principal de navegar entre páginas en NextJS. Proporciona navegación del lado del cliente, lo que significa que la página se carga sin recargar el navegador completo.
Uso básico
import Link from 'next/link'
export default function Navigation() {
return (
<nav>
<Link href="/">Inicio</Link>
<Link href="/productos">Productos</Link>
<Link href="/contacto">Contacto</Link>
</nav>
)
}
Cuando haces click en estos enlaces, NextJS navega sin recargar la página completa, lo que resulta en una navegación mucho más rápida.
Diferencias entre Link y <a>
Etiqueta <a> tradicional:
<a href="/productos">Productos</a>
- Recarga la página completa
- Pierde el estado de la aplicación
- Más lento
- Se descarga todo el JavaScript de nuevo
Componente Link:
<Link href="/productos">Productos</Link>
- Navegación del lado del cliente
- Mantiene el estado
- Más rápido
- Precarga (prefetch) automáticamente
No uses <a> para navegación interna
Solo usa <a> para enlaces externos. Para navegación interna siempre usa Link.
// ✓ Correcto - Link para rutas internas
<Link href="/productos">Productos</Link>
// ✓ Correcto - <a> para enlaces externos
<a href="https://google.com" target="_blank">Google</a>
// ✗ Incorrecto - <a> para rutas internas
<a href="/productos">Productos</a>
Prop href - Destino del enlace
La prop href define a dónde navega el Link. Puede ser una ruta estática o dinámica.
Rutas estáticas
<Link href="/productos">Productos</Link>
<Link href="/sobre-nosotros">Sobre Nosotros</Link>
<Link href="/contacto">Contacto</Link>
Rutas dinámicas
Para rutas con parámetros:
// Ruta: /productos/[id]
<Link href="/productos/123">Ver Producto</Link>
<Link href="/productos/camisa-azul">Ver Camisa Azul</Link>
// Con template strings
const productoId = "123"
<Link href={`/productos/${productoId}`}>Ver Producto</Link>
Objeto de ruta
También puedes pasar un objeto con pathname y query:
<Link
href={{
pathname: '/productos',
query: { categoria: 'ropa', orden: 'precio' },
}}
>
Ver Ropa
</Link>
Resultado: /productos?categoria=ropa&orden=precio
Esto es útil cuando tienes múltiples parámetros de búsqueda:
const filtros = {
categoria: 'ropa',
talla: 'M',
color: 'azul',
orden: 'precio',
}
<Link
href={{
pathname: '/productos',
query: filtros,
}}
>
Filtrar
</Link>
Resultado: /productos?categoria=ropa&talla=M&color=azul&orden=precio
Prefetching - Precarga automática
Una de las características más poderosas de Link es el prefetching (precarga en español). NextJS precarga automáticamente las páginas enlazadas cuando el Link aparece en el viewport (área visible de la pantalla).
Cómo funciona
<Link href="/productos">Productos</Link>
Cuando este Link aparece en pantalla:
- NextJS descarga el código de la página
/productosen segundo plano - Cuando el usuario hace click, la página se muestra instantáneamente
- La navegación es casi instantánea porque todo ya está cargado
Desactivar prefetching
En algunos casos, puede que no quieras prefetch automático:
<Link href="/dashboard" prefetch={false}>
Dashboard
</Link>
Cuándo desactivar prefetch:
- Páginas con mucha data que no se visitan frecuentemente
- Enlaces al final de listas muy largas
- Enlaces detrás de autenticación
Prefetch solo en producción
El prefetching solo funciona en modo producción (npm run build && npm start). En desarrollo (npm run dev) no se precarga para facilitar el desarrollo.
Prefetch manual
Puedes forzar prefetch programáticamente:
'use client'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
export default function Component() {
const router = useRouter()
useEffect(() => {
// Precargar la página de checkout
router.prefetch('/checkout')
}, [router])
return <button>Continuar</button>
}
Esto es útil cuando sabes que el usuario probablemente navegará a cierta página.
Reemplazar el historial
Por defecto, Link agrega una nueva entrada al historial del navegador. Puedes cambiar esto con replace:
// Agrega al historial (default)
<Link href="/productos">Productos</Link>
// Reemplaza la entrada actual
<Link href="/login" replace>
Login
</Link>
Cuándo usar replace:
- Redirecciones después de login
- Flujos de onboarding donde no quieres que el usuario regrese
- Pasos de checkout que no deberían poder retroceder
// Ejemplo: después de login exitoso
<Link href="/dashboard" replace>
Ir al Dashboard
</Link>
El usuario no podrá usar el botón "atrás" para volver a la página de login.
Scroll behavior - Comportamiento del scroll
Por defecto, NextJS hace scroll al top cuando navegas a una nueva página.
Scroll al top (default)
<Link href="/productos">Productos</Link>
Al navegar, la página hace scroll hasta arriba automáticamente.
Mantener posición de scroll
<Link href="/productos" scroll={false}>
Productos
</Link>
La página no hace scroll, mantiene la posición actual.
Cuándo usar scroll={false}:
- Filtros que actualizan la URL pero no la página completa
- Paginación donde quieres mantener la vista
- Tabs que cambian contenido sin scroll
// Ejemplo: filtros de productos
export default function Filtros() {
return (
<div className="filtros">
<Link
href="/productos?categoria=ropa"
scroll={false}
>
Ropa
</Link>
<Link
href="/productos?categoria=zapatos"
scroll={false}
>
Zapatos
</Link>
</div>
)
}
Pasar props a Link
Link solo acepta ciertas props. Para pasar clases CSS u otras props, envuelve el contenido:
// ✗ Incorrecto - className no funciona directamente
<Link href="/productos" className="boton">
Productos
</Link>
// ✓ Correcto - envuelve en un elemento
<Link href="/productos">
<span className="boton">Productos</span>
</Link>
// ✓ También correcto
<Link href="/productos">
<button className="boton">Productos</button>
</Link>
Link con componentes personalizados
// Componente de botón personalizado
function Boton({ children, ...props }) {
return (
<button className="mi-boton" {...props}>
{children}
</button>
)
}
// Usarlo con Link
<Link href="/productos">
<Boton>Ver Productos</Boton>
</Link>
Navegación programática con useRouter
Para navegar mediante código (no clicks), usa el hook useRouter.
Setup básico
'use client'
import { useRouter } from 'next/navigation'
export default function Formulario() {
const router = useRouter()
const handleSubmit = () => {
// Hacer algo...
router.push('/productos')
}
return (
<button onClick={handleSubmit}>
Enviar
</button>
)
}
Client Component requerido
useRouter solo funciona en Client Components. Debes agregar 'use client' al inicio del archivo.
También importa desde 'next/navigation', NO desde 'next/router' (Pages Router antiguo).
Métodos de useRouter
router.push() - Navegar adelante
const router = useRouter()
// Navegar a una ruta
router.push('/productos')
// Con parámetros
router.push('/productos?categoria=ropa')
// Con template string
const id = "123"
router.push(`/productos/${id}`)
Agrega una entrada al historial (puedes usar botón "atrás").
router.replace() - Reemplazar entrada
router.replace('/dashboard')
Reemplaza la entrada actual del historial (no puedes regresar con botón "atrás").
router.back() - Regresar
router.back()
Equivale a presionar el botón "atrás" del navegador.
router.forward() - Avanzar
router.forward()
Equivale a presionar el botón "adelante" del navegador.
router.refresh() - Refrescar
router.refresh()
Refresca la página actual (re-fetching server components).
Ejemplos de uso
Formulario con redirección
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
export default function FormularioProducto() {
const router = useRouter()
const [cargando, setCargando] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setCargando(true)
try {
// Guardar producto
const res = await fetch('/api/productos', {
method: 'POST',
body: JSON.stringify({ /* datos */ }),
})
if (res.ok) {
// Redirigir a lista de productos
router.push('/productos')
}
} catch (error) {
console.error(error)
} finally {
setCargando(false)
}
}
return (
<form onSubmit={handleSubmit}>
{/* Campos del formulario */}
<button type="submit" disabled={cargando}>
{cargando ? 'Guardando...' : 'Guardar'}
</button>
</form>
)
}
Búsqueda con navegación
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
export default function BarraBusqueda() {
const router = useRouter()
const [busqueda, setBusqueda] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
router.push(`/buscar?q=${encodeURIComponent(busqueda)}`)
}
return (
<form onSubmit={handleSubmit}>
<input
type="search"
value={busqueda}
onChange={(e) => setBusqueda(e.target.value)}
placeholder="Buscar productos..."
/>
<button type="submit">Buscar</button>
</form>
)
}
Botón de "volver"
'use client'
import { useRouter } from 'next/navigation'
export default function BotonVolver() {
const router = useRouter()
return (
<button onClick={() => router.back()}>
← Volver
</button>
)
}
Navegación condicional
'use client'
import { useRouter } from 'next/navigation'
export default function BotonComprar({ producto }) {
const router = useRouter()
const handleClick = () => {
if (producto.stock > 0) {
router.push(`/carrito?producto=${producto.id}`)
} else {
alert('Producto agotado')
}
}
return (
<button onClick={handleClick}>
Comprar
</button>
)
}
Detectar ruta activa
Para resaltar el enlace de la página actual, usa usePathname:
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
export default function NavegacionConActivo() {
const pathname = usePathname()
return (
<nav>
<Link
href="/"
className={pathname === '/' ? 'activo' : ''}
>
Inicio
</Link>
<Link
href="/productos"
className={pathname === '/productos' ? 'activo' : ''}
>
Productos
</Link>
<Link
href="/contacto"
className={pathname === '/contacto' ? 'activo' : ''}
>
Contacto
</Link>
</nav>
)
}
Detectar rutas que empiezan con...
Para secciones con sub-rutas:
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
export default function Nav() {
const pathname = usePathname()
// Activo si estás en /productos o /productos/cualquier-cosa
const esProductos = pathname.startsWith('/productos')
return (
<Link
href="/productos"
className={esProductos ? 'activo' : ''}
>
Productos
</Link>
)
}
Pasar estado entre páginas
A veces necesitas pasar datos de una página a otra sin ponerlos en la URL.
Con Router State
'use client'
import { useRouter } from 'next/navigation'
export default function ListaProductos() {
const router = useRouter()
const verDetalle = (producto) => {
// Navegar con estado
router.push('/producto/detalle', { state: { producto } })
}
return (
<div>
{productos.map(producto => (
<button key={producto.id} onClick={() => verDetalle(producto)}>
{producto.nombre}
</button>
))}
</div>
)
}
'use client'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
export default function DetalleProducto() {
const router = useRouter()
const [producto, setProducto] = useState(null)
useEffect(() => {
// Obtener estado pasado
const state = router.state
if (state?.producto) {
setProducto(state.producto)
}
}, [router])
return <div>{producto?.nombre}</div>
}
El estado pasado con router se pierde si el usuario recarga la página. Solo úsalo para datos temporales, no críticos.
Alternativas para persistir datos
Si los datos deben persistir entre recargas:
- Query params en URL:
router.push(`/producto?id=${producto.id}&nombre=${producto.nombre}`)
- LocalStorage:
localStorage.setItem('producto', JSON.stringify(producto))
router.push('/detalle')
- Context API:
// Guardar en contexto global antes de navegar
setProductoActual(producto)
router.push('/detalle')
Middleware para redirecciones
Para redirecciones automáticas basadas en condiciones (como autenticación), usa middleware:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const usuario = request.cookies.get('usuario')
// Si no hay sesión y intenta acceder al dashboard
if (!usuario && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Si ya tiene sesión y está en login
if (usuario && request.nextUrl.pathname === '/login') {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/login'],
}
El middleware intercepta las peticiones antes de que lleguen a la página.
Mejores prácticas
1. Usa Link para navegación interna
// ✓ Correcto
<Link href="/productos">Productos</Link>
// ✗ Incorrecto
<a href="/productos">Productos</a>
<button onClick={() => window.location.href = '/productos'}>
Productos
</button>
2. Usa useRouter para navegación programática
// ✓ Correcto - después de acción
const handleSubmit = () => {
// guardar datos...
router.push('/success')
}
// ✗ Incorrecto - usa Link si es solo navegación
<button onClick={() => router.push('/productos')}>
Productos
</button>
// Mejor:
<Link href="/productos">
<button>Productos</button>
</Link>
3. Desactiva prefetch cuando no sea necesario
// Lista de 1000+ productos
{productos.map(p => (
<Link
href={`/productos/${p.id}`}
prefetch={false} // No precargar todos
key={p.id}
>
{p.nombre}
</Link>
))}
4. Usa replace para flujos sin retorno
// Después de completar un pago
router.replace('/confirmacion') // No puede regresar al checkout
// Después de logout
router.replace('/login') // No puede regresar al dashboard
5. No bloquees la navegación con prompts
// ✗ Evita esto
window.onbeforeunload = () => "¿Seguro que quieres salir?"
// ✓ Mejor: guarda automáticamente
useEffect(() => {
const intervalo = setInterval(() => {
guardarBorrador()
}, 30000) // Auto-guardar cada 30 segundos
return () => clearInterval(intervalo)
}, [])
6. Maneja errores de navegación
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
export default function Component() {
const router = useRouter()
const [error, setError] = useState(null)
const handleNavegacion = async () => {
try {
// Verificar algo antes de navegar
const res = await fetch('/api/verificar')
if (res.ok) {
router.push('/destino')
} else {
setError('No autorizado')
}
} catch (err) {
setError('Error de conexión')
}
}
return (
<div>
{error && <p className="error">{error}</p>}
<button onClick={handleNavegacion}>Continuar</button>
</div>
)
}
Comparación de métodos
| Método | Cuándo usar | Historial | Prefetch |
|---|---|---|---|
<Link> | Navegación normal | Agrega | Sí (auto) |
router.push() | Después de acciones | Agrega | No |
router.replace() | Sin retorno | Reemplaza | No |
router.back() | Botón volver | Retrocede | No |
<a> | Enlaces externos | Agrega | No |
Ejemplo completo - E-commerce
Navegación completa para una tienda online:
// components/Navegacion.tsx
'use client'
import Link from 'next/link'
import { usePathname, useRouter } from 'next/navigation'
import { useState } from 'react'
export default function Navegacion() {
const pathname = usePathname()
const router = useRouter()
const [busqueda, setBusqueda] = useState('')
const handleBusqueda = (e: React.FormEvent) => {
e.preventDefault()
if (busqueda.trim()) {
router.push(`/buscar?q=${encodeURIComponent(busqueda)}`)
}
}
return (
<nav className="navegacion">
{/* Logo - Link a home */}
<Link href="/" className="logo">
Mi Tienda
</Link>
{/* Menú principal */}
<div className="menu">
<Link
href="/productos"
className={pathname.startsWith('/productos') ? 'activo' : ''}
>
Productos
</Link>
<Link
href="/ofertas"
className={pathname === '/ofertas' ? 'activo' : ''}
>
Ofertas
</Link>
<Link
href="/sobre-nosotros"
className={pathname === '/sobre-nosotros' ? 'activo' : ''}
>
Sobre Nosotros
</Link>
</div>
{/* Búsqueda */}
<form onSubmit={handleBusqueda} className="busqueda">
<input
type="search"
value={busqueda}
onChange={(e) => setBusqueda(e.target.value)}
placeholder="Buscar productos..."
/>
<button type="submit">🔍</button>
</form>
{/* Acciones de usuario */}
<div className="acciones">
<Link href="/favoritos">
❤️ Favoritos
</Link>
<Link href="/carrito">
🛒 Carrito (3)
</Link>
<Link href="/cuenta">
👤 Mi Cuenta
</Link>
</div>
</nav>
)
}
// components/TarjetaProducto.tsx
'use client'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
export default function TarjetaProducto({ producto }) {
const router = useRouter()
const agregarAlCarrito = async (e: React.MouseEvent) => {
e.preventDefault() // No navegar al hacer click
// Agregar al carrito
await fetch('/api/carrito', {
method: 'POST',
body: JSON.stringify({ productoId: producto.id }),
})
// Navegar al carrito
router.push('/carrito')
}
return (
<Link
href={`/productos/${producto.slug}`}
className="tarjeta-producto"
>
<img src={producto.imagen} alt={producto.nombre} />
<h3>{producto.nombre}</h3>
<p className="precio">${producto.precio}</p>
<button
onClick={agregarAlCarrito}
className="boton-agregar"
>
Agregar al Carrito
</button>
</Link>
)
}
// app/productos/[slug]/page.tsx
import Link from 'next/link'
import BotonVolver from '@/components/BotonVolver'
import BotonAgregarCarrito from '@/components/BotonAgregarCarrito'
export default async function ProductoPage({ params }) {
const producto = await getProducto(params.slug)
return (
<div>
<BotonVolver />
<h1>{producto.nombre}</h1>
<p>{producto.descripcion}</p>
<p className="precio">${producto.precio}</p>
<BotonAgregarCarrito producto={producto} />
{/* Enlaces relacionados */}
<div className="relacionados">
<h2>Productos Relacionados</h2>
{producto.relacionados.map(rel => (
<Link
key={rel.id}
href={`/productos/${rel.slug}`}
prefetch={false} // No precargar todos
>
{rel.nombre}
</Link>
))}
</div>
{/* Breadcrumbs */}
<nav className="breadcrumbs">
<Link href="/">Inicio</Link>
<span>/</span>
<Link href="/productos">Productos</Link>
<span>/</span>
<Link href={`/productos?categoria=${producto.categoria}`}>
{producto.categoria}
</Link>
<span>/</span>
<span>{producto.nombre}</span>
</nav>
</div>
)
}
Resumen
Navegación declarativa con Link:
- Usa
Linkpara todos los enlaces internos - Prefetch automático mejora el rendimiento
- Soporta todas las props de routing
Navegación programática con useRouter:
- Usa
useRouterdespués de acciones push()agrega al historialreplace()para flujos sin retornoback()yforward()para navegación del navegador
Mejores prácticas:
Linkpara navegación normaluseRouterpara navegación condicional- Desactiva prefetch en listas largas
- Usa
replacepara flujos lineales - Nunca uses
<a>para rutas internas