Navegación
Navega entre rutas usando el componente <Link>, useRouter programático, hooks para leer la URL y server-side redirect.
Recipe
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.
Working Example
// 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>
);
}Deep Dive
How It Works
<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ímiteloading.tsxmá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.useRouterproporciona navegación imperativa.push(),replace(),back(),forward()yrefresh()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 unURLSearchParamsde solo lectura. Para actualizar parámetros de búsqueda, construye una nueva string de URL y usarouter.push().redirect()lanza un error internamente. Debe llamarse fuera de bloquestry/catch(o usaunstable_rethrowen el catch). El código después deredirect()nunca se ejecuta.redirect()usa 307 (temporal) por defecto. Usaredirect(url, RedirectType.replace)para 308 (permanente) o usapermanentRedirect().- Los hooks de navegación requieren Client Components.
useRouter,usePathnameyuseSearchParamstodos requieren la directiva"use client".
Variations
// 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>
);
}TypeScript Notes
// 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>Gotchas
useRouteres denext/navigation, nonext/router. La versión denext/routeres para el Pages Router y no funcionará en App Router.redirect()lanza un error internamente. Si se llama dentro de untry/catch, el catch lo interceptará. Usaunstable_rethrow(error)en bloques catch que podrían atraparlo accidentalmente.useSearchParams()debe envuelto en<Suspense>. En Next.js 15+, usaruseSearchParams()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 retornavoid. UsauseTransitionpara rastrear el estado pendiente.<Link>prefetches en la entrada del viewport. Esto puede causar solicitudes de red inesperadas. Usaprefetch={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.- Los cambios de
searchParamsno activarloading.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.- El prop
scrollen<Link>estruepor defecto. La navegación desplaza hacia arriba por defecto. Establecescroll={false}para navegación tipo pestaña o listas filtradas.
Alternatives
| 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 |
Real-World Example
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:
- Cada función construye un nivel de la jerarquía de URL: service, section, point, tab
- Este archivo es el ÚNICO lugar donde se definen patrones de URL como
/services/.... Cambiar el patrón aquí actualiza toda la app stripServicePrefixystripSectionPrefixlimpian IDs de base de datos que pueden contener prefijos redundantes (normalización de datos heredados)parseServiceUrles la inversa, extrayendo un slug de una URL usando regex- El segmento
pt/en URLs de puntos distingue puntos de secciones en la jerarquía de enrutamiento - Nunca uses template strings para URLs en otro lugar. Los constructores centralizados previenen links rotos cuando los patrones de URL cambian
FAQs
¿Cuál es la diferencia entre Link y useRouter para navegación?
<Link>es declarativa, prefetches por defecto y es el enfoque estándar para la mayoría de navegaciónuseRouteres imperativa, usada para navegación programática después de eventos como envíos de formularios- Ambas realizan navegación en el cliente sin recargas completas de página
Gotcha: ¿Cuál import useRouter deberías usar en el App Router?
Importa desde next/navigation, no next/router. La versión de next/router es para el Pages Router y no funcionará en componentes App Router.
¿Cómo funciona el prefetching de Link?
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.
¿Qué hace router.refresh()?
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.
Gotcha: ¿Qué pasa si redirect() se llama dentro de un try/catch?
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().
¿Por qué debe useSearchParams 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>.
¿Cómo actualizas parámetros de búsqueda sin navegación completa?
"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()}`);¿Los cambios de parámetros de búsqueda activan loading.tsx?
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.
¿Cuál es la diferencia entre redirect() y permanentRedirect()?
redirect()envía un redirect 307 (temporal) por defectopermanentRedirect()envía un redirect 308 (permanente)- Ambas lanzan internamente y el código después de ellas nunca se ejecuta
¿Cuáles son los tipos TypeScript para métodos useRouter?
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): void¿Cómo rastreas el estado pendiente durante navegación programática?
Usa useTransition ya que router.push() retorna void y no awaits.
const [isPending, startTransition] = useTransition();
startTransition(() => {
router.push("/dashboard");
});
// isPending es true durante la navegación¿Qué hace el prop scroll en Link?
Es 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.
¿Cómo useSelectedLayoutSegment ayuda con el resaltado de link activo?
"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>
);
}Related
- App Router Basics — resumen de convenciones de archivo
- Dynamic Routes — enlazando a rutas parametrizadas
- Middleware — redirects y rewrites a nivel de solicitud
- Loading & Error — estados de loading durante navegación
- Intercepting Routes — patrones de navegación modal