Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { Suspense } from "react";
// Envolva componentes assíncronos em limites do Suspense
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Dashboard />
</Suspense>
);
}
// React 19: hook use() para dados baseados em promise
import { use } from "react";
function Dashboard({ dataPromise }: { dataPromise: Promise<DashboardData> }) {
const data = use(dataPromise);
return <DashboardView data={data} />;
}Quando usar isso: Quando componentes precisam esperar por dados assíncronos, código carregado de forma preguiçosa ou conteúdo transmitido pelo servidor. Suspense substitui o estado manual isLoading por limites de carregamento declarativos.
import { Suspense, use, useState, useTransition, lazy, type ReactNode } from "react";
// --- Busca de dados com use() e Suspense ---
interface Post {
id: number;
title: string;
body: string;
}
// Cache para promises de fetch (exemplo simples - use uma biblioteca em produção)
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("Falha ao buscar 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>
);
}
// Loader 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 com múltiplos limites do 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">Falha ao carregar posts.</p>}>
<Suspense fallback={<PostListSkeleton />}>
<PostList postsPromise={fetchPosts(userId)} />
</Suspense>
</ErrorBoundary>
</div>
);
}
// --- Carregamento preguiçoso com 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>
);
}O que isso demonstra:
use() do React 19 consumindo uma promise, acionando Suspense automaticamenteuseTransition para manter a UI atual visível enquanto novos dados carregam (evitando flash de estado de carregamento)ErrorBoundary + Suspense emparelhados para tratamento completo de estado assíncronoisPending para mostrar um indicador de carregamento não bloqueanteReact.lazy e Suspensefallback em vez da subárvore suspensa.use() (React 19) lê o valor de uma promise. Se a promise ainda não foi resolvida, ele suspende o componente.React.lazy() envolve uma importação dinâmica e suspende até que o módulo seja carregado.useTransition envolve atualizações de estado para que o Suspense mostre a UI antiga com um indicador pendente em vez do fallback.| API | Parâmetros | Propósito |
|---|---|---|
<Suspense> | fallback: ReactNode | Mostra o fallback enquanto os filhos suspendem |
use(promise) | Promise<T> | Lê o valor da promise, suspende se estiver pendente |
use(context) | Context<T> | Lê o contexto (pode ser chamado condicionalmente no React 19) |
React.lazy(loader) | () => Promise<{ default: Component }> | Divide o código de um componente |
useTransition() | Nenhum | Retorna [isPending, startTransition] para atualizações não bloqueantes |
startTransition(fn) | () => void | Marca atualizações de estado como não urgentes |
Suspense aninhado para carregamento progressivo:
function ProductPage({ productId }: { productId: string }) {
return (
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails productId={productId} />
{/* Avaliações carregam independentemente, depois */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={productId} />
</Suspense>
</Suspense>
);
}Streaming com Suspense em Server Components (Next.js):
// app/page.tsx - Server Component
export default function Page() {
return (
<main>
<h1>Dashboard</h1>
<Suspense fallback={<ChartSkeleton />}>
{/* Este Server Component assíncrono transmite quando pronto */}
<RevenueChart />
</Suspense>
</main>
);
}
async function RevenueChart() {
const data = await getRevenueData(); // executa no servidor
return <Chart data={data} />;
}use<T>(promise: Promise<T>) retorna T - TypeScript infere corretamente o tipo resolvido.React.lazy espera que a importação retorne { default: ComponentType }. Exportações nomeadas precisam de um wrapper: lazy(() => import('./Foo').then(m => ({ default: m.Foo }))).fallback do Suspense é tipado como ReactNode e aceita null (renderiza nada enquanto carrega).Criar promises durante o render - Chamar fetch() dentro do corpo do componente cria uma nova promise a cada renderização, causando um loop de suspensão infinito. Correção: Crie a promise fora do render (em um manipulador de eventos, componente pai ou cache) e passe-a como prop.
Falta de ErrorBoundary - Se uma promise suspensa rejeitar, o erro se propaga para cima. Sem um boundary de erro, toda a árvore é desmontada. Correção: Sempre emparelhe Suspense com um ErrorBoundary.
Waterfall de carregamento - Limites de Suspense aninhados com buscas de dados sequenciais causam waterfalls (A carrega, então B começa). Correção: Inicie as buscas em paralelo e passe as promises para baixo, ou use uma biblioteca de dados que suporte pré-carregamento paralelo.
Flash de estado de carregamento - Buscas de dados rápidas causam um breve flash do esqueleto. Correção: Use useTransition para continuar mostrando o conteúdo atual, ou use useDeferredValue para valores derivados.
Suspense não captura erros de manipuladores de eventos - Apenas suspensões de renderização são capturadas. Uma função assíncrona em onClick não aciona o Suspense. Correção: Gerencie o estado assíncrono do manipulador de eventos manualmente ou mova a busca de dados para um recurso de suspensão.
| Abordagem | Trade-off |
|---|---|
Suspense + use() | Declarativo, composable; requer disciplina de cache de promises |
useEffect + estado de carregamento | Manual, mas explícito; boilerplate verboso |
| React Query / SWR | Cache completo, revalidação, opção de Suspense; dependência extra |
loading.tsx do Next.js | Limite do Suspense em nível de rota; específico do Next.js |
| UI Esqueleto sem Suspense | Abordagem apenas CSS; sem integração com React |
De uma aplicação SaaS de produção Next.js 15 / React 19 (SystemsArchitect.io).
// Exemplo de produção: página de aterrissagem de estudo com Suspense
// Arquivo: 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>Carregando serviços...</div>}>
<StudyServiceSelector services={services} totalCards={totalCards} />
</Suspense>
</div>
);
}O que isso demonstra em produção:
StudyServiceSelector (um Client Component)fallback em vez da subárvore suspensa.isLoading por limites de carregamento 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() lê o valor de uma promise. Se a promise ainda não foi resolvida, ele suspende o componente.use() infere corretamente o tipo resolvido em TypeScript.React.lazy() envolve uma importação dinâmica e suspende até que o módulo (código do componente) seja carregado. É para divisão de código.use() lê dados de uma promise e suspende até que os dados resolvam. É para busca de dados.useTransition envolve uma atualização de estado como não urgente, mantendo a UI atual visível enquanto os dados carregam.isPending.fetch() dentro do corpo de renderização cria uma nova promise a cada renderização.<ErrorBoundary fallback={<p>Erro</p>}>
<Suspense fallback={<Skeleton />}>
<AsyncComponent />
</Suspense>
</ErrorBoundary><Suspense fallback={<ProductSkeleton />}>
<ProductDetails productId={id} />
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={id} />
</Suspense>
</Suspense>ProductDetails carregue.ProductReviews.const Foo = lazy(() =>
import("./Foo").then((m) => ({ default: m.Foo }))
);React.lazy espera { default: ComponentType } da importação.default.loading.tsx criam limites do Suspense em nível de rota automaticamente.onClick não aciona o Suspense.fallback é tipado como ReactNode e aceita null, que não renderiza nada enquanto carrega.null é apropriado quando você não quer um indicador visual de carregamento (por exemplo, dados pré-buscados que resolvem instantaneamente).useTransition e useDeferredValue para manter a UI responsivaRevisado por Chris St. John·Última atualização: 10 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥