00
:
00
:
00
:
00
Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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

  • data e’ sempre definita (non serve il check if (isPending)).
  • Il loading state e’ gestito dal Suspense boundary 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
    },
  },
})