En esta página
Suspense y Error Boundaries
React.Suspense — UI declarativa para estados de carga
Suspense es la respuesta de React al problema de los estados de carga: en lugar de condicionales if (cargando) dispersos por el código, declaras un fallback en un punto centralizado y React lo muestra automáticamente mientras el contenido no esté listo.
// Sin Suspense: manejo manual del estado de carga en cada componente
function ContenidoManual() {
if (cargando) return <Spinner />;
if (error) return <Error />;
return <Contenido datos={datos} />;
}
// Con Suspense: el fallback es declarativo y reutilizable
<Suspense fallback={<Spinner />}>
<Contenido /> {/* Suspende automáticamente mientras carga */}
</Suspense>El hook use() de React 19
use() es un nuevo hook primitivo que puede desenvolver:
- Promesas — suspende el componente hasta que la promesa resuelve o rechaza
- Contextos — equivalente a
useContextpero con soporte para uso condicional
import { use } from 'react';
// Con promesas
function MiComponente({ promesa }: { promesa: Promise<Datos> }) {
const datos = use(promesa); // Suspende aquí hasta que resuelve
return <div>{datos.titulo}</div>;
}
// Con contexto (puede usarse condicionalmente, a diferencia de useContext)
function MiComponente({ necesitaTema }: { necesitaTema: boolean }) {
if (!necesitaTema) return <div>Sin tema</div>;
const tema = use(TemaContexto); // Válido dentro de un if
return <div className={tema}>Con tema</div>;
}Patrones de Suspense avanzados
Múltiples Suspense boundaries
function PaginaDashboard(): React.JSX.Element {
return (
<div className="dashboard">
{/* Cada sección tiene su propio fallback */}
<Suspense fallback={<EsqueletoHeader />}>
<HeaderStats />
</Suspense>
<div className="dashboard-grid">
<Suspense fallback={<EsqueletoGrafica />}>
<GraficaVentas />
</Suspense>
<Suspense fallback={<EsqueletoTabla />}>
<TablaUltimosOrders />
</Suspense>
</div>
</div>
);
}SuspenseList para coordinar cargas
import { SuspenseList } from 'react';
// SuspenseList coordina el orden en que los fallbacks se revelan
function FeedNoticias({ noticias }: { noticias: Promise<Noticia>[] }) {
return (
<SuspenseList revealOrder="forwards" tail="collapsed">
{noticias.map((promesa, i) => (
<Suspense key={i} fallback={<EsqueletoNoticia />}>
<Noticia promesa={promesa} />
</Suspense>
))}
</SuspenseList>
);
}ErrorBoundary — capturar errores de render
Los Error Boundaries son la única forma de capturar errores en el árbol de renderizado. Deben ser componentes de clase:
import { Component, type ErrorInfo, type ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, info: ErrorInfo) => void;
}
interface Estado {
tieneError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, Estado> {
state: Estado = { tieneError: false, error: null };
static getDerivedStateFromError(error: Error): Estado {
return { tieneError: true, error };
}
componentDidCatch(error: Error, info: ErrorInfo): void {
this.props.onError?.(error, info);
}
render(): ReactNode {
if (this.state.tieneError) {
return this.props.fallback ?? (
<div role="alert" className="error-boundary">
<h2>Error inesperado</h2>
<details>
<summary>Detalles técnicos</summary>
<pre>{this.state.error?.message}</pre>
</details>
<button
type="button"
onClick={() => this.setState({ tieneError: false, error: null })}
>
Intentar de nuevo
</button>
</div>
);
}
return this.props.children;
}
}Combinando ErrorBoundary con Suspense
La combinación correcta es siempre ErrorBoundary envolviendo Suspense:
function ContentoSeguro({ children }: { children: React.ReactNode }) {
return (
<ErrorBoundary
fallback={<p role="alert">Hubo un error cargando este contenido.</p>}
onError={(error) => console.error('Reportando error:', error)}
>
<Suspense fallback={<Skeleton />}>
{children}
</Suspense>
</ErrorBoundary>
);
}
// Uso
<ContentoSeguro>
<ComponenteQueUsaUse />
</ContentoSeguro>React.lazy — code splitting automático
React.lazy permite cargar componentes dinámicamente, dividiendo el bundle:
import { lazy, Suspense } from 'react';
// El import() dinámico crea un chunk separado en el build
const MapaInteractivo = lazy(() => import('./MapaInteractivo'));
const EditorMarkdown = lazy(() =>
import('./EditorMarkdown').then((m) => ({ default: m.EditorMarkdown }))
);
function App(): React.JSX.Element {
const [mostrarMapa, setMostrarMapa] = useState(false);
const [mostrarEditor, setMostrarEditor] = useState(false);
return (
<div>
<button type="button" onClick={() => setMostrarMapa(true)}>Ver mapa</button>
<button type="button" onClick={() => setMostrarEditor(true)}>Abrir editor</button>
{mostrarMapa && (
<ErrorBoundary fallback={<p>Error cargando el mapa</p>}>
<Suspense fallback={<p>Cargando mapa…</p>}>
<MapaInteractivo />
</Suspense>
</ErrorBoundary>
)}
{mostrarEditor && (
<ErrorBoundary>
<Suspense fallback={<p>Cargando editor…</p>}>
<EditorMarkdown />
</Suspense>
</ErrorBoundary>
)}
</div>
);
}Vite divide automáticamente el bundle en chunks cuando usa import() dinámico. El mapa y el editor solo se descargan cuando el usuario los necesita, reduciendo el bundle inicial.
TanStack Query + Suspense
TanStack Query tiene soporte nativo para Suspense:
const { data } = useSuspenseQuery({
queryKey: ['usuario', id],
queryFn: () => fetch(`/api/usuarios/${id}`).then(r => r.json()) as Promise<Usuario>,
// No hay isLoading: el componente se suspende automáticamente
});Con useSuspenseQuery, el componente se suspende mientras carga y lanza un error si la query falla, trabajando perfectamente con Suspense + ErrorBoundary.
Inicia sesión para guardar tu progreso