SWR con React Suspense
Receta
Habilita la opción suspense en useSWR para integrarlo con los límites de React Suspense. Cuando suspense está activo, SWR lanza una promesa durante la carga, permitiendo que <Suspense> gestione la interfaz de respaldo.
"use client";
import { Suspense } from "react";
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
function UserName({ id }: { id: string }) {
// Con suspense: true, data está garantizado como definido
const { data } = useSWR(`/api/users/${id}`, fetcher, { suspense: true });
return <span>{data.name}</span>;
}
export default function Page() {
return (
<Suspense fallback={<div>Cargando usuario...</div>}>
<UserName id="1" />
</Suspense>
);
}Ejemplo funcional
"use client";
import { Suspense } from "react";
import useSWR, { SWRConfig } from "swr";
interface Post {
id: string;
title: string;
body: string;
}
interface Comment {
id: string;
text: string;
author: string;
}
const fetcher = (url: string) => fetch(url).then((r) => r.json());
function PostContent({ postId }: { postId: string }) {
const { data: post } = useSWR<Post>(`/api/posts/${postId}`, fetcher, {
suspense: true,
});
return (
<article>
<h1>{post!.title}</h1>
<p>{post!.body}</p>
</article>
);
}
function PostComments({ postId }: { postId: string }) {
const { data: comments } = useSWR<Comment[]>(
`/api/posts/${postId}/comments`,
fetcher,
{ suspense: true }
);
return (
<ul>
{comments!.map((c) => (
<li key={c.id}>
<strong>{c.author}</strong>: {c.text}
</li>
))}
</ul>
);
}
export default function PostPage({ postId }: { postId: string }) {
return (
<SWRConfig value={{ suspense: true }}>
<Suspense fallback={<div>Cargando publicación...</div>}>
<PostContent postId={postId} />
</Suspense>
<Suspense fallback={<div>Cargando comentarios...</div>}>
<PostComments postId={postId} />
</Suspense>
</SWRConfig>
);
}Profundización
Cómo funciona
- Con
suspense: true, SWR lanza una promesa cuando los datos aún no están disponibles. El límite<Suspense>de React la captura y renderiza el respaldo. - Una vez que la promesa se resuelve, React vuelve a renderizar el componente. En ese momento,
dataestá garantizado como definido (noundefined). - Varias llamadas a
useSWRdentro del mismo límite<Suspense>activan obtenciones en paralelo, pero el límite espera a que todas terminen. - Límites
<Suspense>separados alrededor de cada componente dependiente de datos permiten estados de carga independientes y streaming. - Los errores en modo suspense se lanzan durante el renderizado, por lo que los captura el Error Boundary más cercano.
Variaciones
Suspense global mediante SWRConfig:
<SWRConfig value={{ suspense: true }}>
<Suspense fallback={<Loading />}>
{children}
</Suspense>
</SWRConfig>Suspense anidado para carga progresiva:
<Suspense fallback={<HeaderSkeleton />}>
<Header />
<Suspense fallback={<ContentSkeleton />}>
<MainContent />
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
</Suspense>
</Suspense>Combinado con ErrorBoundary:
import { ErrorBoundary } from "react-error-boundary";
<ErrorBoundary fallback={<div>Algo salió mal</div>}>
<Suspense fallback={<div>Cargando...</div>}>
<DataComponent />
</Suspense>
</ErrorBoundary>Notas de TypeScript
- Con
suspense: true,datasigue tipado comoData | undefinedpor defecto. Usa una aserción non-null o un cast cuando sepas que suspense garantiza la presencia de los datos. - Considera un tipo envoltorio o un hook que restrinja el tipo cuando suspense esté habilitado.
function useSuspenseSWR<T>(key: string, fetcher: (url: string) => Promise<T>) {
const result = useSWR<T>(key, fetcher, { suspense: true });
return { ...result, data: result.data as T };
}
// data está tipado como User, no User | undefined
const { data } = useSuspenseSWR<User>("/api/me", fetcher);Errores comunes
- El modo suspense en SWR sigue evolucionando. Las funciones concurrentes como
useTransitioncon SWR pueden tener casos límite. - Al usar suspense con SSR (Server-Side Rendering), SWR recurrirá a la obtención en el cliente. Necesitas prepoblar la caché con
fallbackenSWRConfigpara evitar desincronizaciones de hidratación. isLoadingeisValidatingtienen semánticas distintas en modo suspense. El componente simplemente no se renderiza mientras carga, así que nunca verásisLoading: truedentro del componente.- Si usas
suspense: truesin un límite<Suspense>, React lanzará un error hasta el Error Boundary más cercano o hará fallar la aplicación. - Las claves condicionales (
null) desactivan el comportamiento de suspense para ese hook. El componente se renderizará condata: undefined. - Las revalidaciones en segundo plano en modo suspense no vuelven a suspender. Solo la carga inicial suspende.
Alternativas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Modo suspense de SWR | Estados de carga limpios, componible | Complejidad con SSR, requiere restringir tipos |
| Comprobaciones manuales de isLoading | Control total, explícito | Código repetitivo de interfaz de carga |
| React Server Components | No requiere estado de carga en el cliente | No puedes usar hooks, sin actualizaciones en tiempo real |
| Hook use() (React 19) | Desenvoltura nativa de promesas | Experimental, API distinta |
FAQs
¿Cómo funciona el modo suspense de SWR por debajo?
Con suspense: true, SWR lanza una promesa cuando los datos aún no están disponibles. El límite <Suspense> de React captura esa promesa, renderiza el respaldo y vuelve a renderizar el componente una vez que la promesa se resuelve.
¿Está garantizado que data esté definido dentro de un componente que usa suspense: true?
Sí, en tiempo de renderizado data está garantizado como disponible porque el componente solo se renderiza después de que la promesa se resuelve. Sin embargo, TypeScript sigue tipándolo como Data | undefined por defecto. Usa una aserción non-null o un hook envoltorio para restringir el tipo.
¿Qué ocurre si uso suspense: true sin un límite Suspense?
React lanzará la promesa hacia arriba. Si no hay un límite <Suspense>, llegará al Error Boundary más cercano o hará fallar la aplicación por completo.
¿Cómo habilito el modo suspense globalmente para todas las llamadas a useSWR?
<SWRConfig value={{ suspense: true }}>
<Suspense fallback={<Loading />}>
{children}
</Suspense>
</SWRConfig>¿Varias llamadas a useSWR dentro del mismo límite Suspense obtienen datos en paralelo?
Sí. Varias llamadas a useSWR dentro del mismo límite <Suspense> activan obtenciones en paralelo. El límite espera a que todas terminen antes de renderizar.
Trampa: ¿Las revalidaciones en segundo plano vuelven a suspender el componente?
No. Solo la carga inicial suspende. Las revalidaciones en segundo plano actualizan los datos en silencio sin volver a activar el respaldo de Suspense.
¿Cómo manejo los errores en modo suspense?
Los errores en modo suspense se lanzan durante el renderizado, por lo que los captura el Error Boundary más cercano. Envuelve tu <Suspense> dentro de un <ErrorBoundary>:
<ErrorBoundary fallback={<div>Error</div>}>
<Suspense fallback={<div>Cargando...</div>}>
<DataComponent />
</Suspense>
</ErrorBoundary>Trampa: ¿Qué ocurre con SSR y el modo suspense de SWR?
SWR recurre a la obtención en el cliente durante SSR. Necesitas prepoblar la caché con fallback en SWRConfig para evitar desincronizaciones de hidratación entre servidor y cliente.
¿Cómo creo un hook suspense con tipos seguros en TypeScript donde data no sea undefined?
function useSuspenseSWR<T>(key: string, fetcher: (url: string) => Promise<T>) {
const result = useSWR<T>(key, fetcher, { suspense: true });
return { ...result, data: result.data as T };
}
// data está tipado como T, no T | undefined¿Qué ocurre cuando uso una clave null con suspense: true?
Las claves condicionales (null) desactivan el comportamiento de suspense para ese hook. El componente se renderiza de inmediato con data: undefined en lugar de suspender.
¿Cómo logro una carga progresiva con límites Suspense anidados?
Envuelve cada sección dependiente de datos en su propio límite <Suspense>. Esto permite estados de carga independientes y streaming, de modo que las secciones más rápidas se renderizan sin esperar a las más lentas.