Navegación y Enlaces en Next.js

Next.js proporciona herramientas poderosas para manejar la navegación de manera eficiente, optimizando automáticamente la carga de páginas y mejorando la experiencia del usuario.

El componente Link es la forma principal de navegar entre páginas en Next.js, proporcionando navegación del lado del cliente.

tsx
// components/Navigation.tsx
import Link from 'next/link'

export function Navigation() {
return (
  <nav className="navigation">
    <Link href="/">
      Inicio
    </Link>
    
    <Link href="/about">
      Acerca de
    </Link>
    
    <Link href="/products">
      Productos
    </Link>
    
    <Link href="/contact">
      Contacto
    </Link>
  </nav>
)
}
tsx
// components/ProductList.tsx
import Link from 'next/link'

interface Product {
id: string
name: string
slug: string
}

export function ProductList({ products }: { products: Product[] }) {
return (
  <div className="product-list">
    {products.map((product) => (
      <div key={product.id} className="product-card">
        <h3>{product.name}</h3>
        
        {/* Ruta dinámica con parámetros */}
        <Link href={`/products/${product.slug}`}>
          Ver detalles
        </Link>
        
        {/* Usando objeto href */}
        <Link 
          href={{
            pathname: '/products/[slug]',
            query: { slug: product.slug }
          }}
        >
          Ver con objeto
        </Link>
      </div>
    ))}
  </div>
)
}

Router Client con App Router

tsx
// app/layout.tsx
'use client'
import { useRouter, usePathname } from 'next/navigation'
import { useEffect } from 'react'

export function ClientNavigation() {
const router = useRouter()
const pathname = usePathname()

// Navegación programática
const handleNavigation = (path: string) => {
  router.push(path)
}

// Detectar cambios de ruta
useEffect(() => {
  console.log('Ruta actual:', pathname)
}, [pathname])

return (
  <div className="client-nav">
    <button onClick={() => handleNavigation('/dashboard')}>
      Ir al Dashboard
    </button>
    
    <button onClick={() => router.back()}>
      Volver
    </button>
    
    <button onClick={() => router.forward()}>
      Adelante
    </button>
    
    <button onClick={() => router.refresh()}>
      Refrescar
    </button>
  </div>
)
}
tsx
// components/AdvancedLink.tsx
import Link from 'next/link'

export function AdvancedLink() {
return (
  <div className="advanced-links">
    {/* Prefetch deshabilitado */}
    <Link href="/heavy-page" prefetch={false}>
      Página pesada (sin prefetch)
    </Link>
    
    {/* Replace en lugar de push */}
    <Link href="/login" replace>
      Login (reemplaza historial)
    </Link>
    
    {/* Scroll deshabilitado */}
    <Link href="/section#content" scroll={false}>
      Ir a sección (sin scroll)
    </Link>
    
    {/* Link externo */}
    <Link 
      href="https://nextjs.org" 
      target="_blank"
      rel="noopener noreferrer"
    >
      Sitio externo
    </Link>
  </div>
)
}
tsx
// components/ActiveLink.tsx
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { ReactNode } from 'react'

interface ActiveLinkProps {
href: string
children: ReactNode
activeClassName?: string
exactMatch?: boolean
}

export function ActiveLink({ 
href, 
children, 
activeClassName = 'active',
exactMatch = false 
}: ActiveLinkProps) {
const pathname = usePathname()

const isActive = exactMatch 
  ? pathname === href
  : pathname.startsWith(href)

return (
  <Link 
    href={href}
    className={`nav-link ${isActive ? activeClassName : ''}`}
  >
    {children}
  </Link>
)
}

// Uso del componente
export function MainNavigation() {
return (
  <nav>
    <ActiveLink href="/" exactMatch>
      Inicio
    </ActiveLink>
    
    <ActiveLink href="/blog">
      Blog
    </ActiveLink>
    
    <ActiveLink href="/products">
      Productos
    </ActiveLink>
  </nav>
)
}
tsx
// components/SearchNavigation.tsx
'use client'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { useState } from 'react'

export function SearchNavigation() {
const router = useRouter()
const searchParams = useSearchParams()
const [query, setQuery] = useState('')

const handleSearch = () => {
  const params = new URLSearchParams(searchParams)
  if (query) {
    params.set('q', query)
  } else {
    params.delete('q')
  }
  
  router.push(`/search?${params.toString()}`)
}

return (
  <div className="search-nav">
    {/* Links con query params */}
    <Link href="/products?category=electronics">
      Electrónicos
    </Link>
    
    <Link href="/products?category=clothing&sort=price">
      Ropa por precio
    </Link>
    
    {/* Búsqueda dinámica */}
    <div className="search-form">
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Buscar productos..."
      />
      <button onClick={handleSearch}>
        Buscar
      </button>
    </div>
  </div>
)
}

3. Hooks de Next.js

useRouter Hook

tsx
// hooks/useNavigation.ts
'use client'
import { useRouter } from 'next/navigation'
import { useTransition } from 'react'

export function useNavigation() {
const router = useRouter()
const [isPending, startTransition] = useTransition()

const navigateTo = (path: string, options?: {
  replace?: boolean
  scroll?: boolean
}) => {
  startTransition(() => {
    if (options?.replace) {
      router.replace(path, { scroll: options.scroll })
    } else {
      router.push(path, { scroll: options.scroll })
    }
  })
}

const goBack = () => {
  startTransition(() => {
    router.back()
  })
}

const refresh = () => {
  startTransition(() => {
    router.refresh()
  })
}

return {
  navigateTo,
  goBack,
  refresh,
  isPending
}
}

usePathname y useSearchParams

tsx
// components/RouteInfo.tsx
'use client'
import { usePathname, useSearchParams } from 'next/navigation'
import { useEffect, useState } from 'react'

export function RouteInfo() {
const pathname = usePathname()
const searchParams = useSearchParams()
const [routeData, setRouteData] = useState({
  path: '',
  params: {} as Record<string, string>
})

useEffect(() => {
  const params: Record<string, string> = {}
  searchParams.forEach((value, key) => {
    params[key] = value
  })

  setRouteData({
    path: pathname,
    params
  })
}, [pathname, searchParams])

return (
  <div className="route-info">
    <h3>Información de la Ruta</h3>
    <p><strong>Pathname:</strong> {routeData.path}</p>
    
    {Object.keys(routeData.params).length > 0 && (
      <div>
        <strong>Parámetros:</strong>
        <ul>
          {Object.entries(routeData.params).map(([key, value]) => (
            <li key={key}>
              {key}: {value}
            </li>
          ))}
        </ul>
      </div>
    )}
  </div>
)
}

Hook personalizado para navegación

tsx
// hooks/useActiveRoute.ts
'use client'
import { usePathname } from 'next/navigation'
import { useMemo } from 'react'

interface RouteConfig {
path: string
label: string
icon?: string
children?: RouteConfig[]
}

export function useActiveRoute(routes: RouteConfig[]) {
const pathname = usePathname()

const activeRoute = useMemo(() => {
  const findActiveRoute = (routeList: RouteConfig[]): RouteConfig | null => {
    for (const route of routeList) {
      if (pathname === route.path) {
        return route
      }
      
      if (route.children) {
        const childMatch = findActiveRoute(route.children)
        if (childMatch) return childMatch
      }
      
      if (pathname.startsWith(route.path + '/')) {
        return route
      }
    }
    return null
  }

  return findActiveRoute(routes)
}, [pathname, routes])

const breadcrumbs = useMemo(() => {
  const segments = pathname.split('/').filter(Boolean)
  return segments.map((segment, index) => ({
    label: segment.charAt(0).toUpperCase() + segment.slice(1),
    path: '/' + segments.slice(0, index + 1).join('/')
  }))
}, [pathname])

return {
  activeRoute,
  breadcrumbs,
  isActive: (path: string) => pathname === path,
  isParentActive: (path: string) => pathname.startsWith(path)
}
}

Optimizaciones automáticas

tsx
// components/OptimizedNavigation.tsx
import Link from 'next/link'

export function OptimizedNavigation() {
return (
  <nav className="optimized-nav">
    {/* 
      Prefetch automático en producción
      Solo para rutas internas
      Se ejecuta cuando el Link entra en viewport
    */}
    <Link href="/dashboard">
      Dashboard (prefetch automático)
    </Link>
    
    {/* 
      Prefetch manual para control granular
    */}
    <Link href="/analytics" prefetch={true}>
      Analytics (prefetch forzado)
    </Link>
    
    {/* 
      Sin prefetch para rutas pesadas
    */}
    <Link href="/reports" prefetch={false}>
      Reportes (sin prefetch)
    </Link>
  </nav>
)
}

Intercepting Routes y Parallel Routes

tsx
// app/@modal/(.)photo/[id]/page.tsx
// Intercepting route para modal
import { Modal } from '@/components/Modal'
import { PhotoDetail } from '@/components/PhotoDetail'

export default function PhotoModal({ 
params 
}: { 
params: { id: string } 
}) {
return (
  <Modal>
    <PhotoDetail id={params.id} />
  </Modal>
)
}

// components/PhotoGallery.tsx
import Link from 'next/link'

export function PhotoGallery({ photos }: { photos: Photo[] }) {
return (
  <div className="photo-gallery">
    {photos.map((photo) => (
      <Link 
        key={photo.id}
        href={`/photo/${photo.id}`}
        className="photo-item"
      >
        <img src={photo.thumbnail} alt={photo.title} />
      </Link>
    ))}
  </div>
)
}

Middleware para navegación

tsx
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl

// Redirección condicional
if (pathname.startsWith('/old-blog')) {
  return NextResponse.redirect(
    new URL('/blog', request.url)
  )
}

// Proteger rutas privadas
if (pathname.startsWith('/dashboard')) {
  const token = request.cookies.get('auth-token')
  
  if (!token) {
    return NextResponse.redirect(
      new URL('/login', request.url)
    )
  }
}

// Reescritura de URL
if (pathname.startsWith('/api/v1')) {
  return NextResponse.rewrite(
    new URL(`/api${pathname.slice(6)}`, request.url)
  )
}

return NextResponse.next()
}

export const config = {
matcher: [
  '/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}

5. Navegación Programática Avanzada

Router con estado y transiciones

tsx
// components/AdvancedRouter.tsx
'use client'
import { useRouter } from 'next/navigation'
import { useTransition, useState } from 'react'

export function AdvancedRouter() {
const router = useRouter()
const [isPending, startTransition] = useTransition()
const [navigationState, setNavigationState] = useState<{
  from: string | null
  to: string | null
  isNavigating: boolean
}>({
  from: null,
  to: null,
  isNavigating: false
})

const navigateWithState = (path: string) => {
  setNavigationState({
    from: window.location.pathname,
    to: path,
    isNavigating: true
  })

  startTransition(() => {
    router.push(path)
    
    // Simular finalización de navegación
    setTimeout(() => {
      setNavigationState(prev => ({
        ...prev,
        isNavigating: false
      }))
    }, 100)
  })
}

return (
  <div className="advanced-router">
    {navigationState.isNavigating && (
      <div className="navigation-loader">
        Navegando de {navigationState.from} a {navigationState.to}...
      </div>
    )}
    
    <button 
      onClick={() => navigateWithState('/products')}
      disabled={isPending}
    >
      {isPending ? 'Navegando...' : 'Ir a Productos'}
    </button>
    
    <button 
      onClick={() => navigateWithState('/services')}
      disabled={isPending}
    >
      {isPending ? 'Navegando...' : 'Ir a Servicios'}
    </button>
  </div>
)
}
tsx
// hooks/useConfirmNavigation.ts
'use client'
import { useRouter } from 'next/navigation'
import { useCallback, useState } from 'react'

export function useConfirmNavigation() {
const router = useRouter()
const [showConfirm, setShowConfirm] = useState(false)
const [pendingNavigation, setPendingNavigation] = useState<string | null>(null)

const navigateWithConfirm = useCallback((
  path: string, 
  message: string = '¿Estás seguro de que quieres salir?'
) => {
  if (window.confirm(message)) {
    router.push(path)
  }
}, [router])

const navigateWithModal = useCallback((path: string) => {
  setPendingNavigation(path)
  setShowConfirm(true)
}, [])

const confirmNavigation = useCallback(() => {
  if (pendingNavigation) {
    router.push(pendingNavigation)
    setPendingNavigation(null)
  }
  setShowConfirm(false)
}, [router, pendingNavigation])

const cancelNavigation = useCallback(() => {
  setPendingNavigation(null)
  setShowConfirm(false)
}, [])

return {
  navigateWithConfirm,
  navigateWithModal,
  confirmNavigation,
  cancelNavigation,
  showConfirm,
  pendingNavigation
}
}

Mejores Prácticas

  Performance

Usa prefetch={false} para rutas pesadas y replace para navegación que no debe agregarse al historial. El prefetch automático mejora la UX significativamente.

  SEO

Siempre usa el componente Link para navegación interna. Los enlaces externos deben incluir target="_blank" y rel="noopener noreferrer" por seguridad.

  Accesibilidad

Implementa estados de carga durante navegación y asegúrate de que los enlaces tengan texto descriptivo. Usa aria-current="page" para enlaces activos.

  Hooks Avanzados

Combina useTransition con navegación para mejor UX. Los hooks usePathname y useSearchParams son esenciales para componentes que dependen de la ruta actual.