Next.js
Navegación declarativa e imperativa
Compara Link, useRouter, redirect y permanentRedirect, junto con prefetch, historial, refresh, search params y seguridad de destinos.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
Next.js
Compara Link, useRouter, redirect y permanentRedirect, junto con prefetch, historial, refresh, search params y seguridad de destinos.
La navegación en Next.js puede expresarse como un enlace, ejecutarse desde código cliente o interrumpir el flujo en servidor. La elección correcta depende de quién inicia el cambio de URL y de si existe un destino semánticamente navegable.
Existen tres familias principales:
<Link>
→ navegación declarativa iniciada por el usuario
useRouter
→ navegación imperativa desde un Client Component
redirect / permanentRedirect
→ control de flujo desde servidor, Action o Route HandlerTodas pueden cambiar la ruta, pero no tienen la misma semántica, accesibilidad ni ciclo de ejecución.
Una navegación interna no necesita descargar un documento HTML completo:
usuario activa Link
↓
router cliente resuelve destino
↓
usa prefetch o solicita RSC payload
↓
React combina el árbol nuevo
↓
layouts compartidos se preservan
↓
historial, scroll y focus se actualizanEl router puede reutilizar segmentos ya conocidos. Los Client Components compatibles conservan state porque React mantiene su identidad.
Una recarga directa es distinta:
browser request
→ documento HTML nuevo
→ scripts
→ hidrataciónNo presupone caches ni estado de navegación anterior.
import Link from "next/link";
export function OrdersNavigation() {
return <Link href="/dashboard/orders">Pedidos</Link>;
}Link renderiza un enlace y conserva comportamientos esperados:
No reemplaces un destino navegable con un botón que llama router.push.
<Link href={`/products/${encodeURIComponent(product.slug)}`}>
{product.name}
</Link>El valor debe pertenecer al dominio esperado. encodeURIComponent protege la estructura del path, pero no valida que el slug sea válido o seguro para tu aplicación.
Para objetos URL:
<Link
href={{
pathname: "/products",
query: { category, page },
}}
>
Ver productos
</Link>Evita construir query strings manualmente cuando varios valores requieren codificación.
En producción, Next.js puede precargar rutas enlazadas cuando entran en viewport:
Link visible
↓
prefetch de segmentos o fallback
↓
click
↓
contenido disponible o streaming inmediatoEl comportamiento depende de si la ruta es estática, dinámica, tiene loading.tsx y de la versión del router.
<Link href="/large-report" prefetch={false}>
Abrir reporte
</Link>Puede ser útil cuando:
No lo desactives globalmente sin medir; el prefetch mejora la respuesta percibida.
Una ruta dinámica con loading.tsx puede prefetchear su shell y fallback, mientras el contenido específico se transmite después.
Sin una boundary útil, la navegación puede esperar más trabajo antes de mostrar feedback.
Diseñar loading forma parte del rendimiento de navegación.
useRouter se usa dentro de Client Components:
"use client";
import { useRouter } from "next/navigation";
export function CreateOrderButton() {
const router = useRouter();
async function handleCreate() {
const order = await createOrder();
router.push(`/orders/${order.id}`);
}
return <button onClick={handleCreate}>Crear pedido</button>;
}La navegación es imperativa porque la URL depende del resultado de una acción.
Añade una entrada al historial:
router.push("/checkout");El botón atrás regresa a la ruta anterior.
Sustituye la entrada actual:
router.replace("/dashboard");Es útil después de login o para normalizar una URL que no debería permanecer en el historial.
Delegan al historial del navegador. No asumas que back() vuelve a una ruta interna conocida; el usuario pudo entrar desde otro sitio.
Solicita una nueva respuesta de servidor para la ruta actual y combina el RSC payload sin perder automáticamente state cliente compatible ni scroll:
router.refresh();No equivale a:
window.location.reload();Tampoco invalida por sí solo todos los datos cacheados. Si la respuesta servidor utiliza la misma caché vigente, puede devolver el mismo contenido.
Permite adelantar una ruta por intención específica:
router.prefetch("/checkout");Úsalo cuando exista evidencia de que el destino es muy probable; no precargues toda la aplicación.
Desde Server Components, Server Actions o Route Handlers:
import { redirect } from "next/navigation";
export default async function SettingsPage() {
const session = await auth();
if (!session) {
redirect("/login");
}
return <Settings user={session.user} />;
}redirect() interrumpe el flujo mediante una excepción interna. No la captures accidentalmente:
try {
await updateUser();
redirect("/profile");
} catch (error) {
// This would also catch the redirect control flow.
}Haz la mutación dentro del try y redirige después.
Indica una redirección permanente cuando la URL canónica cambió:
permanentRedirect(`/users/${newSlug}`);Puede comunicar un status permanente según el contexto. Úsalo para migraciones de URL, no para estado temporal de sesión.
next.config.ts puede definir reglas globales:
const nextConfig: NextConfig = {
async redirects() {
return [
{
source: "/old-pricing",
destination: "/pricing",
permanent: true,
},
];
},
};Son apropiados para rutas conocidas al construir la aplicación. Para millones de redirects administrados por datos, necesitas una estrategia más escalable.
Lee el pathname en un Client Component:
const pathname = usePathname();Útil para navegación activa o analytics. No conviertas un layout servidor completo en cliente por esta razón.
Lee segmentos dinámicos del árbol cliente:
const params = useParams<{ productId: string }>();El genérico no valida la URL en runtime.
Devuelve una interfaz de solo lectura para query string:
const searchParams = useSearchParams();
const query = searchParams.get("query") ?? "";Para modificarla, crea URLSearchParams nuevo y navega:
const next = new URLSearchParams(searchParams.toString());
next.set("query", value);
router.replace(`${pathname}?${next.toString()}`);Filtros, orden, página y tabs compartibles suelen pertenecer a la URL:
/products?category=coffee&page=2Ventajas:
No guardes la misma información en URL y useState sin una regla de sincronización clara.
Una page servidor recibe params y searchParams como Promises en la API actual:
export default async function ProductsPage({
searchParams,
}: PageProps<"/products">) {
const filters = await searchParams;
return <Products filters={parseFilters(filters)} />;
}Usa hooks cliente cuando una región interactiva necesita reaccionar a la navegación. Usa props de page para carga de servidor y rendering inicial.
"use server";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
export async function createCustomer(formData: FormData) {
const input = customerSchema.parse(Object.fromEntries(formData));
const customer = await customerRepository.create(input);
revalidatePath("/customers");
redirect(`/customers/${customer.id}`);
}Flujo:
La Action todavía debe autorizar la operación y ser segura frente a reintentos.
No pases input no confiable a router.push:
router.push(searchParams.get("next")!); // DangerousUna cadena como javascript:... o un dominio externo puede crear XSS o open redirect según el sink.
Valida destinos internos:
function getSafeReturnPath(value: string | null) {
if (!value) return "/dashboard";
if (!value.startsWith("/") || value.startsWith("//")) return "/dashboard";
return value;
}Para URLs externas, usa una allowlist explícita de protocolos y hosts.
La navegación debe mantener:
Un enlace con solo icono necesita nombre accesible. El estado activo puede usar aria-current="page".
Link y router.push gestionan scroll según su configuración:
<Link href="/products" scroll={false}>Actualizar filtros</Link>Desactivar scroll puede ser útil para cambios dentro de la misma pantalla, pero una nueva página suele esperarse desde arriba. Prueba teclado y foco, no solo posición visual.
Rutas interceptadas y slots pueden producir resultados distintos; ambas rutas de entrada necesitan pruebas.
Pierde semántica y comportamiento del navegador. Usa <Link>.
Puede ocultar una estrategia de revalidation imprecisa y generar trabajo amplio.
Impide compartir y usar historial.
La URL sigue siendo input externo.
Puede capturarse como error. Colócalo fuera.
Sacrifica respuesta sin evidencia.
Introduce open redirects o sinks peligrosos.
Link es la opción predeterminada para destinos navegables.useRouter pertenece a decisiones cliente después de una acción.redirect controla flujo servidor.refresh solicita una nueva representación, no limpia toda la caché.router.push no reemplaza siempre a <Link>?router.refresh()?replace en lugar de push?next?Dynamic routes y parámetros desarrolla cómo la URL se transforma en input tipado, validado y utilizado para cargar recursos.