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.

1Componente Link y Router Client

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

El Router Client es una poderosa herramienta para manejar la navegación programática en Next.js.

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

2Navegación: Componente Link Avanzado

El atributo prefetch permite pre-cargar la página destino, mejorando la experiencia del usuario. El atributo replace reemplaza la entrada actual en el historial de navegación, evitando que el usuario regrese a la página anterior.

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

El componente ActiveLink permite agregar una clase activa cuando la ruta coincida con la href.

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

Next.js permite manejar parámetros de búsqueda en las rutas dinámicas. El componente SearchNavigation muestra cómo navegar entre diferentes categorías y cómo realizar búsquedas dinámicas.

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

3Hooks de Next.js

Los hooks de Next.js, como useRouter y useSearchParams, permiten manejar la navegación y los parámetros de búsqueda de manera programática.

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

Next.js proporciona los hooks usePathname y useSearchParams para acceder a la información de la ruta y los parámetros de búsqueda. El componente RouteInfo muestra cómo utilizar estos hooks para mostrar la información de la ruta actual y sus parámetros.

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

Next.js permite crear hooks personalizados para manejar la navegación y la activación de rutas. El hook useActiveRoute muestra cómo detectar la ruta activa y generar breadcrumbs basados en la ruta actual.

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

4Datos Importantes sobre Link

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

Next.js permite interceptar rutas y crear layouts paralelos para mejorar la experiencia de usuario. El ejemplo @modal/(.)photo/[id]/page.tsx muestra cómo interceptar una ruta para mostrar un modal con detalles de una foto. El componente PhotoGallery muestra cómo renderizar una galería de fotos con enlaces a sus detalles.

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

Next.js permite crear middleware para manejar la navegación y las rutas. El middleware middleware.ts muestra cómo redirigir rutas antiguas, proteger rutas privadas y reescribir URLs.

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).*)',
],
}

5Navegación Programática Avanzada

Router con estado y transiciones

Next.js permite crear routers personalizados para manejar la navegación y las transiciones. El ejemplo components/AdvancedRouter.tsx muestra cómo usar useRouter y useTransition para manejar la navegación 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>
)
}

Next.js permite crear hooks personalizados para manejar la navegación con confirmación. El hook useConfirmNavigation muestra cómo mostrar un modal de confirmación antes de navegar a una nueva ruta.

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.