Suspense e SSR
useSuspenseQuery
useSuspenseQuery e’ la versione di useQuery che si integra con React Suspense. Invece di ritornare isPending, sospende il rendering fino a quando i dati sono disponibili.
import { useSuspenseQuery } from '@tanstack/react-query'
import { Suspense } from 'react'
function TodoListContent() {
// Non serve controllare isPending - Suspense gestisce il loading
const { data } = useSuspenseQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
// data e' SEMPRE definita qui (mai undefined)
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
function TodoPage() {
return (
<Suspense fallback={<p>Caricamento todos...</p>}>
<TodoListContent />
</Suspense>
)
}
Vantaggi di useSuspenseQuery
datae’ sempre definita (non serve il checkif (isPending)).- Il loading state e’ gestito dal
Suspenseboundary piu’ vicino. - Puoi comporre piu’ componenti con Suspense in modo dichiarativo.
Suspense Boundaries
Posiziona i Suspense boundary in modo strategico per controllare la granularita’ del loading:
function Dashboard() {
return (
<div className="dashboard">
{/* Un unico fallback per tutta la sidebar */}
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
<main>
{/* Fallback separati per ogni sezione */}
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<DataTable />
</Suspense>
</main>
</div>
)
}
function StatsPanel() {
const { data: stats } = useSuspenseQuery({
queryKey: ['dashboard', 'stats'],
queryFn: fetchStats,
})
return (
<div>
<p>Utenti totali: {stats.totalUsers}</p>
<p>Ordini oggi: {stats.ordersToday}</p>
</div>
)
}
Suspense con Error Boundary
Combina Suspense con ErrorBoundary per gestire sia loading che errori:
import { ErrorBoundary } from 'react-error-boundary'
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<p>Errore in {title}: {error.message}</p>
<button onClick={resetErrorBoundary}>Riprova</button>
</div>
)}
>
<Suspense fallback={<p>Caricamento {title}...</p>}>
{children}
</Suspense>
</ErrorBoundary>
)
}
function App() {
return (
<div>
<Section title="Statistiche">
<StatsPanel />
</Section>
<Section title="Utenti">
<UserList />
</Section>
</div>
)
}
Streaming SSR
TanStack Query supporta lo streaming SSR di React 18+, dove il server invia il HTML progressivamente.
Setup con dehydrate / hydrate
Il pattern base per SSR e’ precaricare i dati sul server, serializzarli (dehydrate) e reidratarli sul client:
// Sul server: precaricare e serializzare
import { dehydrate, QueryClient } from '@tanstack/react-query'
async function renderOnServer() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // evita refetch immediato sul client
},
},
})
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
const dehydratedState = dehydrate(queryClient)
// Passa dehydratedState al client tramite HTML/JSON
return dehydratedState
}
// Sul client: reidratare
import { HydrationBoundary } from '@tanstack/react-query'
function App({ dehydratedState }: { dehydratedState: DehydratedState }) {
const [queryClient] = useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={dehydratedState}>
<Router />
</HydrationBoundary>
</QueryClientProvider>
)
}
Prefetch in Server Components (Next.js App Router)
Con Next.js App Router, puoi precaricare dati direttamente nei Server Components:
// app/posts/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
import { PostList } from './PostList'
export default async function PostsPage() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: async () => {
const res = await fetch('https://api.example.com/posts')
return res.json()
},
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostList />
</HydrationBoundary>
)
}
// app/posts/PostList.tsx (Client Component)
'use client'
import { useSuspenseQuery } from '@tanstack/react-query'
export function PostList() {
const { data: posts } = useSuspenseQuery({
queryKey: ['posts'],
queryFn: async () => {
const res = await fetch('https://api.example.com/posts')
return res.json()
},
})
return (
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Pattern Avanzato con Multiple Prefetch
// app/dashboard/page.tsx
export default async function DashboardPage() {
const queryClient = new QueryClient()
// Prefetch multipli in parallelo
await Promise.all([
queryClient.prefetchQuery({
queryKey: ['dashboard', 'stats'],
queryFn: fetchStats,
}),
queryClient.prefetchQuery({
queryKey: ['dashboard', 'recent-orders'],
queryFn: fetchRecentOrders,
}),
queryClient.prefetchQuery({
queryKey: ['dashboard', 'notifications'],
queryFn: fetchNotifications,
}),
])
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Suspense fallback={<DashboardSkeleton />}>
<DashboardContent />
</Suspense>
</HydrationBoundary>
)
}
Importante: staleTime per SSR
Quando usi SSR, imposta sempre uno staleTime maggiore di zero. Altrimenti i dati saranno considerati stale immediatamente e il client fara’ un refetch non necessario:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // almeno 1 minuto
},
},
})