Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { Suspense } from "react";
// Envuelve componentes asincronos en límites de Suspense
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Dashboard />
</Suspense>
);
}
// React 19: hook use() para datos basados en promesas
import { use } from "react";
function Dashboard({ dataPromise }: { dataPromise: Promise<DashboardData> }) {
const data = use(dataPromise);
return <DashboardView data={data} />;
}Cuándo usarlo: Cuando los componentes necesitan esperar datos asincronos, código cargado dinámicamente o contenido transmitido por el servidor. Suspense reemplaza el estado manual isLoading con límites de carga declarativos.
import { Suspense, use, useState, useTransition, lazy, type ReactNode } from "react";
// --- Obtención de datos con use() y Suspense ---
interface Post {
id: number;
title: string;
body: string;
}
// Caché para promesas de fetch (ejemplo simple - usa una biblioteca en producción)
const cache = new Map<string, Promise<Post[]>>();
function fetchPosts(userId: number): Promise<Post[]> {
const key = `posts-${userId}`;
if (!cache.has(key)) {
cache.set(
key,
fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`)
.then((res) => {
if (!res.ok) throw new Error("Failed to fetch posts");
return res.json();
})
);
}
return cache.get(key)!;
}
function PostList({ postsPromise }: { postsPromise: Promise<Post[]> }) {
const posts = use(postsPromise);
return (
<ul className="space-y-4">
{posts.map((post) => (
<li key={post.id} className="border rounded-lg p-4">
<h3 className="font-semibold">{post.title}</h3>
<p className="text-gray-600 mt-1">{post.body}</p>
</li>
))}
</ul>
);
}
// Cargador esqueleto
function PostListSkeleton() {
return (
<div className="space-y-4">
{Array.from({ length: 3 }, (_, i) => (
<div key={i} className="border rounded-lg p-4 animate-pulse">
<div className="h-5 bg-gray-200 rounded w-3/4 mb-2" />
<div className="h-4 bg-gray-200 rounded w-full" />
<div className="h-4 bg-gray-200 rounded w-5/6 mt-1" />
</div>
))}
</div>
);
}
// --- Página con múltiples límites de Suspense ---
function UserDashboard() {
const [userId, setUserId] = useState(1);
const [isPending, startTransition] = useTransition();
const handleUserChange = (id: number) => {
startTransition(() => {
setUserId(id);
});
};
return (
<div className="max-w-2xl mx-auto p-6">
<nav className="flex gap-2 mb-6">
{[1, 2, 3].map((id) => (
<button
key={id}
onClick={() => handleUserChange(id)}
className={`px-4 py-2 rounded ${
userId === id ? "bg-blue-600 text-white" : "bg-gray-100"
} ${isPending ? "opacity-50" : ""}`}
>
User {id}
</button>
))}
</nav>
<ErrorBoundary fallback={<p className="text-red-600">Failed to load posts.</p>}>
<Suspense fallback={<PostListSkeleton />}>
<PostList postsPromise={fetchPosts(userId)} />
</Suspense>
</ErrorBoundary>
</div>
);
}
// --- Carga dinámica con Suspense ---
const Settings = lazy(() => import("./Settings"));
const Analytics = lazy(() => import("./Analytics"));
function AppRoutes() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/settings" element={<Settings />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
);
}Lo que esto demuestra:
use() de React 19 consumiendo una promesa, disparando Suspense automáticamenteuseTransition para mantener la interfaz actual visible mientras se cargan nuevos datos (evitando un destello del estado de carga)ErrorBoundary + Suspense emparejados para un manejo completo del estado asincrónicoisPending para mostrar un indicador de carga no bloqueanteReact.lazy y Suspensefallback en lugar del subárbol suspendido.use() (React 19) lee el valor de una promesa. Si la promesa aún no se ha resuelto, suspende el componente.React.lazy() envuelve una importación dinámica y se suspende hasta que el módulo se carga.useTransition envuelve actualizaciones de estado para que Suspense muestre la interfaz anterior con un indicador pendiente en lugar del fallback.| API | Parámetros | Propósito |
|---|---|---|
<Suspense> | fallback: ReactNode | Muestra fallback mientras los componentes secundarios se suspenden |
use(promise) | Promise<T> | Lee el valor de la promesa, se suspende si está pendiente |
use(context) | Context<T> | Lee contexto (puede ser llamado condicionalmente en React 19) |
React.lazy(loader) | () => Promise<{ default: Component }> | Divide un componente en código |
useTransition() | Ninguno | Devuelve [isPending, startTransition] para actualizaciones no bloqueantes |
startTransition(fn) | () => void | Marca actualizaciones de estado como no urgentes |
Suspense anidado para carga progresiva:
function ProductPage({ productId }: { productId: string }) {
return (
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails productId={productId} />
{/* Las reseñas se cargan independientemente, más tarde */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={productId} />
</Suspense>
</Suspense>
);
}Transmisión de componentes de servidor con Suspense (Next.js):
// app/page.tsx - Componente de servidor
export default function Page() {
return (
<main>
<h1>Panel de Control</h1>
<Suspense fallback={<ChartSkeleton />}>
{/* Este componente de servidor asincrónico se transmite cuando está listo */}
<RevenueChart />
</Suspense>
</main>
);
}
async function RevenueChart() {
const data = await getRevenueData(); // se ejecuta en servidor
return <Chart data={data} />;
}use<T>(promise: Promise<T>) devuelve T - TypeScript infiere correctamente el tipo resuelto.React.lazy espera que la importación devuelva { default: ComponentType }. Las exportaciones nombradas necesitan un envoltorio: lazy(() => import('./Foo').then(m => ({ default: m.Foo }))).fallback de Suspense se escribe como ReactNode y acepta null (no renderiza nada mientras se carga).Crear promesas durante el renderizado - Llamar a fetch() dentro del cuerpo del componente crea una nueva promesa en cada renderizado, causando un bucle de suspensión infinito. Solución: Crea la promesa fuera del renderizado (en un manejador de eventos, padre o caché) y pásala como prop.
ErrorBoundary faltante - Si una promesa suspendida se rechaza, el error se propaga hacia arriba. Sin un límite de error, todo el árbol se desmonta. Solución: Siempre empareja Suspense con un ErrorBoundary.
Carga en cascada - Los límites de Suspense anidados con obtención de datos secuencial causan cascadas (A se carga, luego B comienza). Solución: Inicia las obtenciones en paralelo y pasa promesas hacia abajo, o usa una biblioteca de datos que admita precarga en paralelo.
Destello del estado de carga - Las obtenciones rápidas de datos causan un breve destello del esqueleto. Solución: Usa useTransition para mantener el contenido actual visible, o usa useDeferredValue para valores derivados.
Suspense no captura errores de manejadores de eventos - Solo se capturan las suspensiones de renderizado. Una función asincrónica en onClick no dispara Suspense. Solución: Administra manualmente el estado asincrónico del manejador de eventos o mueve la obtención de datos a un recurso suspendible.
| Enfoque | Compensación |
|---|---|
Suspense + use() | Declarativo, componible; requiere disciplina de almacenamiento en caché de promesas |
useEffect + estado de carga | Manual pero explícito; boilerplate extenso |
| React Query / SWR | Almacenamiento en caché completo, revalidación, Suspense opcional; dependencia adicional |
loading.tsx de Next.js | Límite de Suspense a nivel de ruta; específico de Next.js |
| Interfaz esquelética sin Suspense | Enfoque solo CSS; sin integración de React |
De una aplicación SaaS de Next.js 15 / React 19 en producción (SystemsArchitect.io).
// Ejemplo de producción: página de destino de estudio con Suspense
// Archivo: src/app/study/page.tsx
import { Suspense } from "react";
import StudyServiceSelector from "@/components/study/study-service-selector";
export default async function StudyLandingPage() {
const availableServices = await getAvailableStudyServices();
const services = availableServices.map((service) => ({
slug: service.serviceSlug,
title: service.serviceTitle,
platform: service.platform,
basicsCount: service.categories.find((c) => c.category === "basics")?.cardCount || 0,
featuresCount: service.categories.find((c) => c.category === "features")?.cardCount || 0,
bestPracticesCount: service.categories.find((c) => c.category === "best-practices")?.cardCount || 0,
}));
const totalCards = availableServices.reduce((sum, s) => sum + s.totalCards, 0);
return (
<div className="relative min-h-screen">
<Suspense fallback={<div>Loading services...</div>}>
<StudyServiceSelector services={services} totalCards={totalCards} />
</Suspense>
</div>
);
}Lo que esto demuestra en producción:
StudyServiceSelector (un componente del cliente)fallback en lugar del subárbol suspendido.isLoading con límites de carga declarativos.function PostList({ postsPromise }: { postsPromise: Promise<Post[]> }) {
const posts = use(postsPromise);
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}use() lee el valor de una promesa. Si la promesa aún no se ha resuelto, suspende el componente.use() infiere correctamente el tipo resuelto en TypeScript.React.lazy() envuelve una importación dinámica y se suspende hasta que el módulo (código del componente) se carga. Es para división de código.use() lee datos de una promesa y se suspende hasta que los datos se resuelven. Es para obtención de datos.useTransition envuelve una actualización de estado como no urgente, manteniendo la interfaz actual visible mientras se cargan los datos.isPending.fetch() dentro del cuerpo del renderizado crea una nueva promesa en cada renderizado.<ErrorBoundary fallback={<p>Error</p>}>
<Suspense fallback={<Skeleton />}>
<AsyncComponent />
</Suspense>
</ErrorBoundary><Suspense fallback={<ProductSkeleton />}>
<ProductDetails productId={id} />
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={id} />
</Suspense>
</Suspense>ProductDetails se carga.ProductReviews.const Foo = lazy(() =>
import("./Foo").then((m) => ({ default: m.Foo }))
);React.lazy espera { default: ComponentType } de la importación.default.loading.tsx crean automáticamente límites de Suspense a nivel de ruta.onClick no dispara Suspense.fallback se escribe como ReactNode y acepta null, que no renderiza nada mientras se carga.null es apropiado cuando no quieres un indicador visual de carga (ej., datos precargados que se resuelven instantáneamente).useTransition y useDeferredValue para mantener la interfaz responsivaRevisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥