Next.js Moderno Mapeado: Qué reemplaza cada función y cuándo usarla
Un recorrido guiado por ocho funcionalidades de Next.js, desde los Componentes del Servidor hasta la API de Metadatos, que muestra qué patrones antiguos reemplaza cada una y dónde pueden causar problemas.
Next.js ahora se encarga del enrutamiento, la obtención de datos, el caché, la estrategia de renderizado y gran parte de las funcionalidades relacionadas con la API. Sin embargo, los equipos a menudo lo adoptan manteniendo viejos hábitos: obtener datos en useEffect, crear rutas de API manualmente para cada formulario, y utilizar reglas de caché que nadie puede explicar. Esta guía aborda ocho capacidades, el patrón anterior que cada una reemplaza, y los detalles que causan problemas en proyectos reales, para que pueda decidir, funcionalidad por funcionalidad, qué debe incluirse en su código.
Por qué el framework ahora abarca toda la capa tecnológica
Walmart, Nike, TikTok, OpenAI y Airbnb ejecutan aplicaciones web con Next.js, y la ventaja radica en la consolidación: el enrutamiento, el empaquetado, los modos de renderizado, el acceso a datos y los puntos finales del servidor comparten un mismo repositorio y un conjunto de convenciones. Elegir un enrutador o configurar un empaquetador ya no forma parte del inicio de un proyecto, por lo que ese tiempo se destina al producto en sí.
1. Los componentes del servidor convierten al servidor en el lugar predeterminado para la renderización
Los React Server Components (RSC) representan el cambio arquitectónico más significativo de esta lista. Un componente del servidor se ejecuta únicamente en el servidor y envía el resultado renderizado al navegador, por lo que su código nunca forma parte del paquete de JavaScript del cliente.
Qué desaparece de una página basada en datos
En React clásico con renderización en el cliente, incluso un componente que solo lee datos e imprime una lista agrega su código, su lógica de obtención de datos y sus dependencias al paquete, para luego activarse en el navegador. Con RSC, el componente lee los datos en el servidor y envía únicamente el resultado. En App Router, cada componente es un componente del servidor a menos que se indique lo contrario, y por eso el archivo siguiente no necesita ninguna directiva en absoluto:
// app/products/page.tsx
// This component runs ONLY on the server. No "use client" directive needed.
La página es una función async que consulta la base de datos directamente y devuelve marcado. No hay ruta API intermedia ni solicitud del lado del cliente:
import { db } from "@/lib/db";export default async function ProductsPage() {
// Direct database access. No API route. No fetch boilerplate.
const products = await db.product.findMany({ take: 20 }); return (
<main>
<h1>Our Products</h1>
<ul>
{products.map((product) => (
<li key={product.id}>
<h2>{product.name}</h2>
<p>${product.price}</p>
</li>
))}
</ul>
</main>
);
}
Ningún useState, ningún useEffect, ninguna bandera de carga, ni fetch a tu propio backend. Se ejecuta menos código en el navegador, el HTML llega completo (lo cual es bueno para los motores de búsqueda) y hay menos cosas que mantener. Dado que este código accede directamente a la base de datos, nunca lo importes a un archivo del cliente; un módulo de datos exclusivo para el servidor deja esa separación clara.
Dónde aún pertenecen los componentes del cliente
Aún necesitas componentes del cliente para cualquier cosa interactiva: controladores de eventos, estado, efectos y APIs del navegador como localStorage. Marcas tal archivo colocando la directiva "use client" en la parte superior:
// components/AddToCartButton.tsx
"use client";
El botón de abajo mantiene un estado local y reacciona a los clics, que es exactamente el tipo de operación que debe realizarse en el navegador:
import { useState } from "react";export function AddToCartButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false); return (
<button onClick={() => setAdded(true)}>
{added ? "Added!" : "Add to Cart"}
</button>
);
}
Comience con los Componentes de Servidor y cámbieles solo donde sea necesario por la interactividad, colocando "use client" lo más abajo posible en la estructura: una página de servidor que incluye un pequeño botón del cliente requiere mucho menos JavaScript que una página que es un Componente de Cliente desde el principio. Para conocer en mayor profundidad cómo funciona el modelo de renderizado, consulte la arquitectura detrás del renderizado sin bundle.
2. Las Acciones de Servidor reemplazan las rutas API para las mutaciones
Las acciones del servidor te permiten escribir una función que se ejecuta en el servidor y llamarla directamente desde un componente. Next.js genera el punto de extremo HTTP, serializa los argumentos y devuelve el resultado, por lo que ya no es necesario mantener una ruta separada solo para aceptar un envío de formulario.
El patrón de dos archivos que deja obsoleto
Anteriormente, una mutación requería un manejador en la carpeta api del Pages Router que leyera el cuerpo de la solicitud, escribiera en la base de datos y respondiera con JSON:
// You needed an API route
// pages/api/create-post.ts
export default async function handler(req, res) {
const { title, content } = req.body;
await db.post.create({ data: { title, content } });
res.status(200).json({ success: true });
}
Luego, el componente tenía que llamar a ese punto de extremo manualmente, serializando el propio payload:
// Then in your component:
const response = await fetch("/api/create-post", {
method: "POST",
body: JSON.stringify({ title, content }),
});
Una acción validada ubicada junto a su formulario
Con las acciones del servidor, la página importa lo que necesita, incluido Zod para la validación de entradas:
// app/posts/new/page.tsx
import { redirect } from "next/navigation";
import { db } from "@/lib/db";
import { z } from "zod";
La acción se declara junto al formulario. La directiva "use server" dentro del cuerpo de la función la marca como código exclusivo para el servidor; el formulario lo pasa a su propiedad action. La acción valida FormData con safeParse, devuelve errores de los campos si la validación falla y, de lo contrario, escribe los datos enviados y redirige:
const schema = z.object({
title: z.string().min(3, "Title must be at least 3 characters"),
content: z.string().min(10, "Content is too short"),
});async function createPost(formData: FormData) {
"use server"; const parsed = schema.safeParse({
title: formData.get("title"),
content: formData.get("content"),
}); if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors };
} await db.post.create({ data: parsed.data });
redirect("/posts");
}export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Post title" required />
<textarea name="content" placeholder="Write something..." required />
<button type="submit">Publish</button>
</form>
);
}
La validación es esencial porque una acción de servidor es un endpoint público al que cualquiera puede acceder con datos arbitrarios. Por la misma razón, las verificaciones de autorización deben realizarse dentro de la propia acción; por qué las acciones de servidor necesitan autorización dentro de cada cuerpo de función explica esto en detalle. También hay que tener en cuenta que aquí no se lee el objeto de error devuelto, por lo que las validaciones fallidas no se muestran. El siguiente patrón soluciona este problema.
Mostrar errores y estado pendiente con useActionState
Cuando el formulario debe mostrar mensajes de validación o deshabilitar el botón mientras se procesa una solicitud, colóquelo en un Componente Cliente y envuelva la acción con el gancho useActionState de React:
"use client";
El gancho devuelve el estado más reciente generado por la acción, un formAction envuelto para pasarlo al formulario, y una bandera isPending. El componente muestra el primer error por campo e intercambia la etiqueta del botón mientras se ejecuta el envío:
import { useActionState } from "react";
import { createPost } from "./actions";export function PostForm() {
const [state, formAction, isPending] = useActionState(createPost, null); return (
<form action={formAction}>
<input name="title" placeholder="Post title" />
{state?.error?.title && (
<p className="text-red-500">{state.error.title[0]}</p>
)}
<textarea name="content" placeholder="Write something..." />
{state?.error?.content && (
<p className="text-red-500">{state.error.content[0]}</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? "Publishing..." : "Publish"}
</button>
</form>
);
}
Un detalle que a menudo causa confusión: cuando se utiliza una acción a través de useActionState, React la llama con el estado anterior como primer argumento y FormData como segundo. La función createPost mostrada anteriormente solo acepta formData, por lo que una versión exportada desde ./actions para este hook necesita la firma (prevState, formData). La acción también debe encontrarse en un archivo separado con "use server" al principio, ya que un componente de cliente no puede definir funciones de servidor de forma inline.
3. Turbopack acorta el ciclo de retroalimentación en el desarrollo
Durante mucho tiempo, webpack marcó el ritmo del desarrollo local. Fast Refresh resultaba práctico una vez que el servidor estaba en funcionamiento, pero los arranques en aplicaciones grandes podían tardar de 30 a 60 segundos. Turbopack es un bundler basado en Rust de Vercel diseñado para eliminar ese cuello de botella.
Turbopack logró una tasa de éxito del 100% en las 8,298 pruebas de integración con Next.js, y los equipos informan que los arranques en desarrollo ahora toman menos de 3 segundos en proyectos que anteriormente necesitaban más de un minuto. Su etiqueta de madurez cambia rápidamente entre versiones, por lo que consulte la documentación actual para conocer su estado en su versión, especialmente en cuanto a las compilaciones para producción.
Activarlo
Para habilitarlo en el desarrollo basta con establecer una sola bandera en la script de desarrollo:
// package.json
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build"
}
}
No se necesita ninguna configuración adicional. Turbopack calcula de forma incremental, reconstruyendo solo lo que ha cambiado, por lo que los proyectos más grandes se benefician al máximo. Si depende de cargadores o complementos personalizados de webpack, primero confirme los equivalentes en Turbopack. Nuestra comparación de herramientas de empaquetado analiza esas consideraciones.
4. Preprocesamiento parcial combina una carcasa estática con datos en flujo
El preprocesamiento parcial (PPR) sirve de inmediato una carcasa HTML estática ya preparada y transmite en flujo las partes dinámicas de la misma página dentro de una sola respuesta.
En una página de producto, el diseño, la navegación y la descripción son los mismos para todos; el precio personalizado, el carrito de compras y la cantidad en stock no lo son. PPR envía la carcasa de inmediato y completa las partes específicas para cada solicitud a medida que se resuelven.
La página importa un componente estático y dos dinámicos:
// app/product/[id]/page.tsx
import { Suspense } from "react";
import { ProductDetails } from "./ProductDetails"; // static
import { PersonalizedPrice } from "./PersonalizedPrice"; // dynamic
import { StockStatus } from "./StockStatus"; // dynamic
El límite entre lo estático y lo dinámico se define con Suspense. Todo lo que está fuera de este límite puede ser prerrenderizado; cada solución alternativa Suspense se convierte en un marcador de posición en la estructura que es reemplazado cuando su contenido hijo finaliza su renderizado en el servidor:
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<div>
{/* This renders statically - instant */}
<ProductDetails id={params.id} /> {/* These stream in dynamically */}
<Suspense fallback={<div>Loading price...</div>}>
<PersonalizedPrice productId={params.id} />
</Suspense> <Suspense fallback={<div>Checking stock...</div>}>
<StockStatus productId={params.id} />
</Suspense>
</div>
);
}
Tenga en cuenta que params se define aquí como un objeto simple. En las versiones recientes de Next.js, params se pasa como una Promise y debe esperarse, por lo que ajuste esa firma según la versión a la que se dirige.
PPR se introdujo mediante una bandera experimental en la configuración de Next.js, comenzando con la importación del tipo:
// next.config.ts
import type { NextConfig } from "next";
y luego la propia bandera:
const nextConfig: NextConfig = {
experimental: {
ppr: true,
},
};export default nextConfig;
En el momento de escribir esto, esta opción ha sido reorganizada en las versiones más recientes (el comportamiento PPR está vinculado a la configuración Cache Components en Next.js 16), por lo que considere este fragmento como ilustrativo y verifique el nombre actual de la opción. De cualquier manera, la página parece estática porque su estructura se almacena en caché, mientras que sus datos permanecen actualizados. Explicamos con más detalle estos mecanismos en pre-renderizado parcial y renderizado concurrente explicado.
5. La directiva use cache hace explícita la caché
Next.js 13 y 14 cacheban de forma agresiva por defecto, y muchos equipos encontraron páginas obsoletas en producción sin ninguna causa aparente. Next.js 16 adopta un enfoque de caché opcional mediante Cache Components y la directiva use cache: usted indica qué elementos deben cachearse en lugar de adivinar cuáles ya lo están.
Al colocar la directiva en la parte superior de un componente asíncrono, se almacena en caché su salida renderizada:
// A component that caches its output for 1 hour
async function PopularArticles() {
"use cache";
El resto del componente carga y renderiza como de costumbre:
const articles = await fetch("https://api.example.com/popular-articles").then(
(r) => r.json()
); return (
<ul>
{articles.map((article: { id: string; title: string }) => (
<li key={article.id}>{article.title}</li>
))}
</ul>
);
}
El comentario menciona una hora, pero la directiva por sí sola no establece una duración; la vida útil proviene de un perfil de caché, aplicado con cacheLife, y de lo contrario se recurre a un perfil predeterminado. Los perfiles personalizados se declaran en la configuración:
// next.config.ts
const nextConfig = {
experimental: {
cacheLife: {
"stale-for-a-day": {
stale: 60 * 60, // 1 hour
revalidate: 60 * 60 * 24, // 1 day
expire: 60 * 60 * 24 * 7, // 1 week
},
},
},
};
Los tres valores responden a preguntas diferentes. stale indica cuánto tiempo puede usar un cliente su copia sin verificar con el servidor, revalidate indica con qué frecuencia el servidor actualiza la entrada en segundo plano, y expire es el momento después del cual la entrada se descarta y la siguiente solicitud debe esperar datos actualizados. Si cacheLife se encuentra bajo la categoría experimental depende de su versión. Para la invalidación basada en etiquetas, que será necesaria una vez que los datos cambien al escribirlos, consulte nuestra guía sobre el uso del caché y la revalidación basada en etiquetas.
6. Funciones de streaming de IA con el AI SDK
El Vercel AI SDK se integra con los Route Handlers y los hooks de React, por lo que el chat en streaming, la búsqueda asistida por IA y las interfaces de usuario generadas se convierten en código de aplicación normal.
En el servidor, un manejador de rutas importa streamText y un proveedor de modelos:
// app/api/chat/route.ts
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
El manejador POST lee la conversación del cuerpo de la solicitud, inicia una transmisión en flujo y la devuelve como respuesta en streaming (el corchete de cierre de la función está cortado en el fragmento):
export async function POST(req: Request) {
const { messages } = await req.json(); const result = streamText({
model: openai("gpt-4o"),
messages,
}); return result.toDataStreamResponse();
En el cliente, la página de chat es un componente de cliente porque almacena el estado de entrada:
// app/chat/page.tsx
"use client";
El hook useChat gestiona la lista de mensajes, el valor de entrada y su envío, y vuelve a renderizarse a medida que llegan nuevos tokens:
import { useChat } from "ai/react";export default function ChatPage() {
const { messages, input, handleInputChange, handleSubmit } = useChat(); return (
<div>
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} placeholder="Ask anything..." />
<button type="submit">Send</button>
</form>
</div>
);
}
Eso equivale a unas 30 líneas para un chat en tiempo real, con un manejo de rutas en el backend y useChat en el frontend. Tenga en cuenta que la API del SDK de IA evoluciona rápidamente: en las versiones más recientes, el hook se importa desde @ai-sdk/react, usted mismo gestiona el estado de entrada, y el helper de respuestas tiene un nombre diferente. Asegúrese de usar versiones fijas y consulte la documentación del SDK antes de copiar este código. Para interfaces tipo agente con herramientas y múltiples pasos, consulte cómo crear interfaces de usuario para agentes de IA con múltiples pasos usando Next.js y el AI SDK.
7. Patrones del App Router para layouts complejos
El App Router, introducido en Next.js 13, es ahora la forma estándar de desarrollar aplicaciones con Next.js. Dos de sus características reemplazan la necesidad anterior de gestionar el estado de forma personalizada.
Rutas paralelas para paneles de panel de control independientes
Las rutas paralelas muestran varias páginas dentro de un mismo diseño al mismo tiempo. Cada carpeta precedida por @ define un slot con nombre:
app/
dashboard/
@analytics/
page.tsx
@recent/
page.tsx
layout.tsx
page.tsx
El diseño recibe cada slot como un parámetro junto con children y los coloca en la cuadrícula:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
recent,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
recent: React.ReactNode;
}) {
return (
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2">{children}</div>
<aside>
{analytics}
{recent}
</aside>
</div>
);
}
Dado que cada slot es un segmento de ruta independiente, carga sus propios datos y puede tener sus propios estados de carga y error. Una consulta de análisis lenta no afecta al panel de actividades recientes. Un problema común: cuando se navega a una subruta que un slot no define, Next.js necesita un default.tsx en ese slot para saber qué renderizar al volver a cargar el sitio.
Interceptación de rutas para modales con URLs reales
Un patrón de interfaz común abre un elemento en un modal mientras la URL cambia a esa referencia, permitiendo así su compartición. Las rutas interceptadas gestionan esto mediante convenciones de carpetas:
app/
photos/
[id]/
page.tsx // Full page view at /photos/123
(..)[id]/
page.tsx // Intercepted modal view
page.tsx
Cuando un usuario hace clic desde la cuadrícula, la carpeta interceptadora captura la navegación del lado del cliente y muestra la versión en modal. Cuando alguien abre directamente la URL o la vuelve a cargar, se muestra la página completa habitual, sin necesidad de trucos manuales con el historial. Los marcadores (.), (..) y (...) se refieren a segmentos de ruta, no a carpetas del sistema de archivos, y el modal generalmente se muestra a través de un slot @modal paralelo; por lo tanto, consulte la documentación de enrutamiento para conocer el diseño exacto que requiere su estructura.
8. Primitivas de SEO: metadatos e imágenes
La API de metadatos convierte el SEO en código normal que se encuentra junto a la página que describe. Después de importar el tipo Metadata:
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
una página exporta generateMetadata, el cual carga la publicación y devuelve el título, la descripción, los datos de Open Graph y la tarjeta de Twitter:
export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata> {
const post = await getPost(params.slug); return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.coverImage }],
type: "article",
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
};
}
Las vistas previas en redes sociales, las URLs canónicas y los datos estructurados provienen de los mismos datos que muestra la página. Si el componente de la página también llama a getPost, elimina la duplicación de solicitudes (por ejemplo, con cache de React) para que la base de datos no sea consultada dos veces.
Para imágenes, next/image debería ser la opción por defecto:
import Image from "next/image";
Por defecto, se carga de forma perezosa, proporciona variantes del tamaño adecuado y se convierte a WebP o AVIF cuando el navegador los soporta. Establecer explícitamente width y height también reserva espacio y evita cambios en el diseño:
export function ProductCard({ product }: { product: Product }) {
return (
<div>
<Image
src={product.imageUrl}
alt={product.name}
width={400}
height={300}
priority={false} // set true for above-the-fold images
/>
<h2>{product.name}</h2>
</div>
);
}
Establezca priority en true solo para la imagen que es visible arriba del despliegue, típicamente la imagen principal o de portada, para que el navegador la descargue temprano.
Un orden de aprendizaje sensato
Cada paso aquí se basa en el anterior:
- Convenciones de nombres de archivos en App Router:
layout.tsx,page.tsx,loading.tsxyerror.tsx. - Componentes del servidor y dónde se encuentra el límite del cliente.
- Acciones del servidor con validación mediante Zod para cada mutación.
- Suspense y transmisión en tiempo real para estados de carga declarativos.
Puntos clave
- Trate al servidor como entorno de ejecución por defecto e implique
"use client"hasta en las partes interactivas más pequeñas. - Las acciones del servidor son puntos de extremo: valide la entrada y verifique los permisos dentro de cada una.
- Dibuje sus límites estáticos y dinámicos con
Suspense; tanto PPR como el caché se basan en ellos. - Prefiera un caché explícito con
use cachey perfiles nombradoscacheLifeen lugar de depender de los valores predeterminados. - Muchas de estas API han cambiado entre las versiones recientes, así que confirme las banderas y firmas según la versión que realmente está utilizando.
Tanto si comienza desde cero como si migra una aplicación grande al App Router, adoptar estos elementos uno por uno es un enfoque de bajo riesgo.
Lecturas relacionadas
- 20 patrones avanzados de Next.js 16 para la arquitectura de aplicaciones a nivel senior — Un resumen del diseño basado en el servidor, el caché, la transmisión en flujo, PPR, rutas paralelas e interceptadoras, y otros patrones para crear aplicaciones escalables con Next.js 16.
- Entendiendo los componentes de caché y el precargue parcial en Next.js 16.3 — Explica cómo la función de navegaciones instantáneas de Next.js 16.3 utiliza shells de rutas compartidos y decisiones explícitas de transmisión en flujo para que las aplicaciones renderizadas en servidor parezcan instantáneas.