En esta página

Suspense y Error Boundaries

12 min lectura TextoCap. 4 — Datos y formularios

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:

  1. Promesas — suspende el componente hasta que la promesa resuelve o rechaza
  2. Contextos — equivalente a useContext pero 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.

use() puede llamarse condicionalmente, a diferencia de useEffect
El hook use() de React 19 rompe una regla de los hooks ordinarios: puede llamarse dentro de condicionales y bucles. Esto es posible porque React lo trata diferente internamente. Sin embargo, solo puede usarse en componentes client-side o en Server Components.
ErrorBoundary no captura errores en event handlers
Los Error Boundaries solo capturan errores durante el render, en métodos del ciclo de vida, y en constructores. Los errores en event handlers (onClick, onChange) no son capturados: debes manejarlos con try-catch dentro del handler. Tampoco captura errores en código asíncrono (await).
Combina múltiples Suspense para cargas granulares
No necesitas un solo Suspense global. Coloca múltiples Suspense con fallbacks específicos alrededor de secciones individuales. Así, el sidebar puede cargarse independientemente del contenido principal, mejorando la percepción de velocidad.