Navegación
Navega entre rutas usando el componente <Link>, useRouter programático, hooks para leer la URL y server-side redirect.
Busca en todas las páginas de la documentación
Navega entre rutas usando el componente <Link>, useRouter programático, hooks para leer la URL y server-side redirect.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
// Navegación declarativa
import Link from "next/link";
<Link href="/dashboard">Dashboard</Link>
// Navegación programática (Client Component)
"use client";
import { useRouter } from "next/navigation";
const router = useRouter();
router.push("/dashboard");
// Lee la URL actual (Client Component)
import { usePathname, useSearchParams } from "next/navigation";
const pathname = usePathname(); // "/dashboard"
const searchParams = useSearchParams(); // URLSearchParams instance
// Redirect en el servidor (Server Component o Server Action)
import { redirect } from "next/navigation";
redirect("/login");Cuándo usarlo: Cada vez que necesites moverte entre páginas, leer la URL actual o redirigir usuarios según condiciones.
// components/nav-bar.tsx - Navegación con resaltado de link activo
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
const links = [
{ href: "/", label: "Home" },
{ href: "/dashboard", label: "Dashboard" },
{ href: "/settings", label: "Settings" },
];
export function NavBar() {
const pathname = usePathname();
return (
<nav className="flex gap-4 border-b px-6 py-3">
{links.map((link) => (
<Link
key={link.href}
href={link.href}
className={
pathname === link.href
? "font-bold text-blue-600"
: "text-gray-600 hover:text-gray-900"
}
>
{link.label}
</Link>
))}
</nav>
);
}// components/search-filter.tsx - Parámetros de búsqueda para filtrar
"use client";
import { useSearchParams, usePathname, useRouter } from "next/navigation";
import { useCallback } from "react";
export function SearchFilter() {
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
const currentQuery = searchParams.get("q") ?? "";
const currentSort = searchParams.get("sort") ?? "newest";
const updateParams = useCallback(
(key: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
params.set(key, value);
} else {
params.delete(key);
}
router.push(`${pathname}?${params.toString()}`);
},
[searchParams, pathname, router]
);
return (
<div className="flex gap-4">
<input
type="text"
placeholder="Search..."
defaultValue={currentQuery}
onChange={(e) => updateParams("q", e.target.value)}
className="rounded border px-3 py-2"
/>
<select
value={currentSort}
onChange={(e) => updateParams("sort", e.target.value)}
className="rounded border px-3 py-2"
>
<option value="newest">Newest</option>
<option value="oldest">Oldest</option>
<option value="popular">Popular</option>
</select>
</div>
);
}// app/login/page.tsx - Redirect en el servidor después de verificar auth
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
export default async function LoginPage() {
const session = await auth();
if (session) redirect("/dashboard");
return (
<form action="/api/auth/login" method="POST">
<input name="email" type="email" placeholder="Email" />
<input name="password" type="password" placeholder="Password" />
<button type="submit">Log In</button>
</form>
);
}// Navegación programática después de enviar un formulario
"use client";
import { useRouter } from "next/navigation";
import { useState, useTransition } from "react";
export function CreatePostForm() {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [title, setTitle] = useState("");
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const res = await fetch("/api/posts", {
method: "POST",
body: JSON.stringify({ title }),
headers: { "Content-Type": "application/json" },
});
const post = await res.json();
startTransition(() => {
router.push(`/blog/${post.slug}`);
router.refresh(); // Revalida datos del servidor
});
}
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Post title"
className="rounded border px-3 py-2"
/>
<button
type="submit"
disabled={isPending}
className="ml-2 rounded bg-blue-600 px-4 py-2 text-white"
>
{isPending ? "Creating..." : "Create"}
</button>
</form>
);
}<Link> prefetches por defecto. Cuando un <Link> entra en el viewport, Next.js prefetches la ruta en el background. Las rutas estáticas se prefetchen completamente; las rutas dinámicas prefetchen hasta el límite loading.tsx más cercano.<Link> realiza navegación en el cliente. Intercepta el click, actualiza la URL y renderiza la nueva ruta sin recarga completa de la página. Solo los segmentos cambiados se re-renderizan.useRouter proporciona navegación imperativa. push(), replace(), back(), forward() y refresh() te permiten navegar programáticamente.router.refresh() vuelve a obtener datos del servidor. Revalida los Server Components de la ruta actual sin perder el estado en el cliente (valores de input, posición de scroll).usePathname() retorna la ruta actual. Es reactiva - el componente se re-renderiza cuando la URL cambia. No incluye parámetros de búsqueda o hash.useSearchParams() retorna un URLSearchParams de solo lectura. Para actualizar parámetros de búsqueda, construye una nueva string de URL y usa router.push().redirect() lanza un error internamente. Debe llamarse fuera de bloques try/catch (o usa unstable_rethrow en el catch). El código después de redirect() nunca se ejecuta.redirect() usa 307 (temporal) por defecto. Usa redirect(url, RedirectType.replace) para 308 (permanente) o usa permanentRedirect().useRouter, usePathname y useSearchParams todos requieren la directiva "use client".// Link con href dinámico y control de prefetch
import Link from "next/link";
// Desactiva prefetch para links raramente visitados
<Link href="/terms" prefetch={false}>Terms</Link>
// href dinámico con template literal
<Link href={`/blog/${slug}`}>Read More</Link>
// Link con replace (sin nueva entrada de historial)
<Link href="/dashboard" replace>Dashboard</Link>
// Link con scroll={false} para preservar posición de scroll
<Link href="/dashboard?tab=settings" scroll={false}>Settings Tab</Link>// permanentRedirect - redirect 308
import { permanentRedirect } from "next/navigation";
export default async function OldPage() {
permanentRedirect("/new-page");
}// redirect en Server Actions
"use server";
import { redirect } from "next/navigation";
export async function createPost(formData: FormData) {
const post = await db.post.create({
data: { title: formData.get("title") as string },
});
redirect(`/blog/${post.slug}`);
}// useSelectedLayoutSegment - lee segmento hijo activo
"use client";
import { useSelectedLayoutSegment } from "next/navigation";
export function TabNav() {
const segment = useSelectedLayoutSegment();
// En /dashboard/analytics → segment = "analytics"
// En /dashboard → segment = null
return (
<nav>
<Link
href="/dashboard"
className={segment === null ? "font-bold" : ""}
>
Overview
</Link>
<Link
href="/dashboard/analytics"
className={segment === "analytics" ? "font-bold" : ""}
>
Analytics
</Link>
</nav>
);
}// Link component props
import type { LinkProps } from "next/link";
// Props principales: href (string o UrlObject), replace, scroll, prefetch
// useRouter return type
import { useRouter } from "next/navigation";
// router.push(href: string, options?: { scroll?: boolean }): void
// router.replace(href: string, options?: { scroll?: boolean }): void
// router.refresh(): void
// router.back(): void
// router.forward(): void
// router.prefetch(href: string): void
// redirect function signature
import { redirect, permanentRedirect, RedirectType } from "next/navigation";
// redirect(url: string, type?: RedirectType): never
// permanentRedirect(url: string, type?: RedirectType): never
// URL object para hrefs complejos
<Link
href={{
pathname: "/blog/[slug]",
query: { slug: "hello-world" },
}}
>
Read Post
</Link>useRouter es de next/navigation, no next/router. La versión de next/router es para el Pages Router y no funcionará en App Router.redirect() lanza un error internamente. Si se llama dentro de un try/catch, el catch lo interceptará. Usa unstable_rethrow(error) en bloques catch que podrían atraparlo accidentalmente.useSearchParams() debe envuelto en <Suspense>. En Next.js 15+, usar useSearchParams() sin un límite Suspense hace que toda la ruta se opte por renderizado en el cliente. Envuelve el componente que lo usa en <Suspense>.router.push() no awaits. La navegación es asincrónica pero el método retorna void. Usa useTransition para rastrear el estado pendiente.<Link> prefetches en la entrada del viewport. Esto puede causar solicitudes de red inesperadas. Usa prefetch={false} para links que los usuarios raramente hacen clic.router.refresh() no limpia la caché del router. Solo vuelve a obtener los datos del servidor de la página actual. Otras rutas cacheadas permanecen obsoletas.searchParams no activar loading.tsx. Solo los cambios de segmento (ruta diferente) activan el límite de loading. Los cambios de parámetros de búsqueda se renderizan sin UI de loading.redirect() en un layout se aplica a todas las páginas hijo. Ten cuidado al colocar redirects condicionales en layouts - se ejecutan para cada ruta hijo.scroll en <Link> es true por defecto. La navegación desplaza hacia arriba por defecto. Establece scroll={false} para navegación tipo pestaña o listas filtradas.| Approach | Cuándo Usar |
|---|---|
<a> tag | Links externos o cuando necesitas una recarga completa de página |
window.location | Escapar del SPA - recarga completa a una URL diferente |
| Middleware redirect | Redireccionar antes de cualquier renderizado |
next.config.js redirects | Reglas de redirección estáticas sin lógica de runtime |
Server Action con redirect() | Redireccionar después de una mutación |
revalidatePath o revalidateTag | Refrescar datos sin navegación |
De una aplicación SaaS Next.js 15 / React 19 en producción (SystemsArchitect.io).
// Production example: Constructor centralizado de URLs
// File: src/lib/url-builder.ts
import { generateSlug } from './slug-utils';
export function buildServiceUrlFromSlug(slug: string): string {
return `/services/${slug}`;
}
export function buildSectionUrl(serviceSlug: string, sectionId: string): string {
const cleanSectionId = stripServicePrefix(sectionId, serviceSlug);
return `/services/${serviceSlug}/${cleanSectionId}`;
}
export function buildPointUrl(
serviceSlug: string,
sectionId: string,
pointSlug: string
): string {
const cleanSectionId = stripServicePrefix(sectionId, serviceSlug);
const cleanPointSlug = stripSectionPrefix(pointSlug, sectionId);
return `/services/${serviceSlug}/${cleanSectionId}/pt/${cleanPointSlug}`;
}
export function buildPointTabUrl(
serviceSlug: string,
sectionId: string,
pointSlug: string,
tabSlug: string | null
): string {
const baseUrl = buildPointUrl(serviceSlug, sectionId, pointSlug);
if (tabSlug === null) return baseUrl;
return `${baseUrl}/${tabSlug}`;
}
export function parseServiceUrl(url: string): string | null {
const match = url.match(/^\/services\/([^\/]+)\/?$/);
return match ? match[1] : null;
}Lo que esto demuestra en producción:
/services/.... Cambiar el patrón aquí actualiza toda la appstripServicePrefix y stripSectionPrefix limpian IDs de base de datos que pueden contener prefijos redundantes (normalización de datos heredados)parseServiceUrl es la inversa, extrayendo un slug de una URL usando regexpt/ en URLs de puntos distingue puntos de secciones en la jerarquía de enrutamiento<Link> es declarativa, prefetches por defecto y es el enfoque estándar para la mayoría de navegaciónuseRouter es imperativa, usada para navegación programática después de eventos como envíos de formulariosImporta desde next/navigation, no next/router. La versión de next/router es para el Pages Router y no funcionará en componentes App Router.
Cuando un <Link> entra en el viewport, Next.js prefetches la ruta en el background. Las rutas estáticas se prefetchen completamente. Las rutas dinámicas prefetchen hasta el límite loading.tsx más cercano. Usa prefetch={false} para links raramente visitados.
Vuelve a obtener los Server Components de la ruta actual sin perder el estado en el cliente (valores de input, posición de scroll). No limpia la caché del router para otras rutas.
redirect() lanza un error internamente, así que el bloque catch lo intercepta. Usa unstable_rethrow(error) en bloques catch que podrían atrap accidentalmente redirect() o notFound().
En Next.js 15+, usar useSearchParams() sin un límite <Suspense> hace que toda la ruta se opte por renderizado en el cliente. Envuelve el componente que lo usa en <Suspense>.
"use client";
import { useSearchParams, usePathname, useRouter } from "next/navigation";
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
const params = new URLSearchParams(searchParams.toString());
params.set("sort", "newest");
router.push(`${pathname}?${params.toString()}`);No. Solo los cambios de segmento (ruta diferente) activan el límite de loading. Los cambios de parámetros de búsqueda se renderizan sin mostrar la UI de loading.
redirect() envía un redirect 307 (temporal) por defectopermanentRedirect() envía un redirect 308 (permanente)import { useRouter } from "next/navigation";
const router = useRouter();
// router.push(href: string, options?: { scroll?: boolean }): void
// router.replace(href: string, options?: { scroll?: boolean }): void
// router.refresh(): void
// router.back(): void
// router.forward(): void
// router.prefetch(href: string): voidUsa useTransition ya que router.push() retorna void y no awaits.
const [isPending, startTransition] = useTransition();
startTransition(() => {
router.push("/dashboard");
});
// isPending es true durante la navegaciónEs true por defecto, desplazándose hasta la parte superior de la página en la navegación. Establece scroll={false} para navegación tipo pestaña o listas filtradas donde quieres preservar la posición de scroll.
"use client";
import { useSelectedLayoutSegment } from "next/navigation";
export function TabNav() {
const segment = useSelectedLayoutSegment();
// /dashboard/analytics -> "analytics"
// /dashboard -> null
return (
<Link
href="/dashboard/analytics"
className={segment === "analytics" ? "font-bold" : ""}
>
Analytics
</Link>
);
}Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥