React 19: Server Components, useOptimistic e Concurrent Mode in Produzione

React 19 non è solo un aggiornamento incrementale: è un cambio di paradigma. Con l’introduzione stabile dei React Server Components, dell’hook useOptimistic, delle Server Actions e del Concurrent Mode in produzione, il modo in cui costruiamo interfacce moderne cambia radicalmente. In questa guida ti mostro come usare queste feature in un progetto reale, con codice pratico e pattern testati.

💡 Prerequisito: questa guida assume familiarità con React 18 e con il concetto di componenti funzionali. Per l’integrazione con Next.js App Router, fai riferimento alla nostra guida su Next.js 16.

Cosa sono i React Server Components (RSC)

I React Server Components sono componenti che girano esclusivamente sul server: non inviano nessun JavaScript al client, possono accedere direttamente al database, al filesystem e a API private, e non hanno accesso a useState, useEffect o eventi browser. Il risultato è un bundle client drasticamente più piccolo e rendering più veloce.

// app/posts/page.tsx — questo è un Server Component per default
import { db } from '@/lib/db'

export default async function PostsPage() {
  // Accesso diretto al DB — zero waterfall, zero bundle client
  const posts = await db.post.findMany({
    orderBy: { createdAt: 'desc' },
    take: 20,
  })

  return (
    <main>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </main>
  )
}

// app/posts/[id]/LikeButton.tsx — Client Component
'use client'
import { useState } from 'react'

export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes)
  return (
    <button onClick={() => setLikes(l => l + 1)}>
      ❤️ {likes}
    </button>
  )
}

Pattern comuni RSC:

  • Layout.tsx — sempre RSC, carica sessione e dati globali
  • Page.tsx — RSC per dati, poi delega a Client Components per interattività
  • Errori comuni: importare useState in un RSC, usare document o window, passare funzioni non serializzabili come props

🔧 Regola pratica: se un componente ha bisogno di interattività (click, input, stato locale) → 'use client'. Tutto il resto resta Server Component per default in Next.js App Router.

useOptimistic — UI ottimistica senza boilerplate

L’hook useOptimistic risolve uno dei pattern più verbosi del frontend: aggiornare l’UI prima che la risposta del server arrivi, e poi riconciliare se qualcosa va storto. In React 19 è integrato nativamente.

'use client'
import { useOptimistic, useTransition } from 'react'
import { addComment } from '@/actions/comments'

type Comment = { id: string; text: string; pending?: boolean }

export function CommentList({ initialComments }: { initialComments: Comment[] }) {
  const [isPending, startTransition] = useTransition()
  const [comments, addOptimisticComment] = useOptimistic(
    initialComments,
    (state, newComment: Comment) => [...state, newComment]
  )

  async function handleSubmit(formData: FormData) {
    const text = formData.get('text') as string
    // UI aggiornata immediatamente
    addOptimisticComment({ id: crypto.randomUUID(), text, pending: true })
    // Server action in background
    await addComment(text)
  }

  return (
    <div>
      {comments.map(c => (
        <div key={c.id} style={{ opacity: c.pending ? 0.6 : 1 }}>
          {c.text}
          {c.pending && <span> (invio...)</span>}
        </div>
      ))}
      <form action={handleSubmit}>
        <input name="text" placeholder="Aggiungi commento..." />
        <button type="submit">Invia</button>
      </form>
    </div>
  )
}

L’effetto percepito è un’app istantanea: l’utente vede il commento apparire subito, con opacità ridotta durante il pending. Se il server fallisce, React ripristina automaticamente lo stato precedente.

Server Actions e il nuovo hook use()

Le Server Actions permettono di chiamare codice server direttamente da un componente client, senza definire API route esplicite. Si definiscono con la direttiva 'use server' e possono essere passate come action a un form, o chiamate come normali funzioni async.

// app/actions/user.ts
'use server'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'

const updateSchema = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email(),
})

export async function updateUserProfile(formData: FormData) {
  // Validazione server-side sempre
  const parsed = updateSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
  })
  if (!parsed.success) {
    return { error: parsed.error.flatten().fieldErrors }
  }

  await db.user.update({
    where: { id: getCurrentUserId() },
    data: parsed.data,
  })

  revalidatePath('/profile')
  return { success: true }
}

Il nuovo hook use() permette di “unwrappare” promesse e context in modo sincrono all’interno di un componente, compatibile con Suspense:

import { use, Suspense } from 'react'

// use() con promesse
function UserName({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise) // Suspense gestisce il loading
  return <span>{user.name}</span>
}

// use() con context — funziona anche dentro condizionali
function ThemeToggle() {
  const theme = use(ThemeContext)
  return <button>{theme === 'dark' ? '☀️' : '🌙'}</button>
}

🎯 Nota TypeScript: per tipizzare correttamente le Server Actions con TypeScript 5, usa ReturnType<typeof yourAction> per inferire i tipi di ritorno e FormData per gli input.

useFormStatus, useFormState e form moderni

React 19 porta due hook specifici per i form che semplificano enormemente la gestione dello stato di caricamento e dei risultati delle Server Actions.

'use client'
import { useFormStatus, useActionState } from 'react'
import { updateUserProfile } from '@/actions/user'

// SubmitButton usa useFormStatus internamente
function SubmitButton() {
  const { pending } = useFormStatus()
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Salvataggio...' : 'Salva profilo'}
    </button>
  )
}

export function ProfileForm({ user }: { user: User }) {
  const [state, formAction] = useActionState(updateUserProfile, null)

  return (
    <form action={formAction}>
      {state?.error && (
        <div className="error">{state.error.name?.[0]}</div>
      )}
      {state?.success && <div className="success">Profilo aggiornato!</div>}

      <input name="name" defaultValue={user.name} />
      <input name="email" defaultValue={user.email} />
      <SubmitButton />
    </form>
  )
}

Concurrent Mode in produzione: Suspense e startTransition

Il Concurrent Mode in React 19 è finalmente stabile e raccomandato in produzione. Il principio chiave è che React può ora interrompere, riprendere e scartare rendering in corso, garantendo che l’UI rimanga sempre reattiva anche durante operazioni pesanti.

import { Suspense, startTransition, useTransition } from 'react'

// Pattern Suspense — no più loading globali
export default function Dashboard() {
  return (
    <div>
      <Suspense fallback={<UserSkeleton />}>
        <UserCard /> {/* RSC — fetch dati utente */}
      </Suspense>

      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart /> {/* RSC — dati analytics */}
      </Suspense>

      <Suspense fallback={<TableSkeleton />}>
        <OrdersTable /> {/* RSC — lista ordini */}
      </Suspense>
    </div>
  )
}

// startTransition — navigazione non bloccante
'use client'
function SearchBar() {
  const [isPending, startTransition] = useTransition()
  const [query, setQuery] = useState('')

  function handleSearch(value: string) {
    setQuery(value)
    startTransition(() => {
      // Questo aggiornamento ha priorità bassa
      // React non blocca l'input mentre calcola i risultati
      router.push(`/search?q=${value}`)
    })
  }

  return (
    <input
      value={query}
      onChange={e => handleSearch(e.target.value)}
      style={{ opacity: isPending ? 0.7 : 1 }}
    />
  )
}

Migrazione da React 18: React 19 è retrocompatibile. Il passaggio principale riguarda la rimozione delle API legacy come ReactDOM.render() (già deprecato in React 18) e l’adozione dei nuovi pattern RSC. Le breaking change sono minime per chi era già su React 18 con la configurazione corretta. Per integrazione con Next.js App Router, vedi la docs ufficiale su react.dev.

FAQ — React 19

Devo riscrivere tutto per usare React 19?

No. React 19 è retrocompatibile con React 18. Puoi adottare le nuove feature gradualmente: inizia aggiungendo useOptimistic ai form esistenti, poi converti i componenti statici in RSC dove ha senso.

I Server Components funzionano senza Next.js?

Tecnicamente sì, ma richiedono un bundler configurato per supportarli (Vite, Webpack con plugin specifici). In pratica, oggi Next.js App Router è il modo più semplice e completo per usare RSC in produzione.

useOptimistic vs useMutation di React Query — quando usare quale?

Se usi Server Actions con Next.js, useOptimistic è la scelta naturale e non richiede dipendenze esterne. Se la tua app ha logica di fetching complessa, cache condivisa tra componenti e retry automatici, React Query rimane la scelta migliore con il proprio supporto per optimistic updates.

Le Server Actions sono sicure?

Sì, ma richiedono validazione server-side come qualsiasi endpoint. Non fidarti mai dei dati del client: usa sempre Zod o una libreria di validazione equivalente prima di toccare il database.

Condividi

Articoli Recenti

Categorie popolari