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 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.

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:

  1. NextJS descarga el código de la página /productos en segundo plano
  2. Cuando el usuario hace click, la página se muestra instantáneamente
  3. 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>
  )
}

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>
// 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>

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>
  )
}
'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:

  1. Query params en URL:
router.push(`/producto?id=${producto.id}&nombre=${producto.nombre}`)
  1. LocalStorage:
localStorage.setItem('producto', JSON.stringify(producto))
router.push('/detalle')
  1. 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

// ✓ 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étodoCuándo usarHistorialPrefetch
<Link>Navegación normalAgregaSí (auto)
router.push()Después de accionesAgregaNo
router.replace()Sin retornoReemplazaNo
router.back()Botón volverRetrocedeNo
<a>Enlaces externosAgregaNo

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 Link para todos los enlaces internos
  • Prefetch automático mejora el rendimiento
  • Soporta todas las props de routing

Navegación programática con useRouter:

  • Usa useRouter después de acciones
  • push() agrega al historial
  • replace() para flujos sin retorno
  • back() y forward() para navegación del navegador

Mejores prácticas:

  • Link para navegación normal
  • useRouter para navegación condicional
  • Desactiva prefetch en listas largas
  • Usa replace para flujos lineales
  • Nunca uses <a> para rutas internas