Stack · React 19

React 19, usato come disciplina e non come rincorsa

Le Actions sostituiscono il boilerplate dei form, useOptimistic sostituisce il plugin di optimistic state che hai smesso di mantenere, i Server Components escono dallo stadio sperimentale. Rilasciamo React 19 in produzione oggi e migriamo codebase React 18 senza rompere il team.

Perché questo stack

01

Actions al posto del boilerplate dei form

Un form ora è una funzione, non un custom hook. `useState`, `onSubmit`, `setError`, `setSubmitting`: i tre pezzi di stato che scrivevi su ogni form collassano in una sola Action. Il compiler gestisce il pending state, l'error state, il success state. Il form torna a fare il proprio mestiere.

02

useOptimistic al posto di una libreria di state-management per una sola feature

Un aggiornamento ottimistico voleva dire una slice Redux, uno store Zustand o un reducer custom. Adesso è un hook di una riga. La UI si aggiorna subito, il server risponde, l'hook riconcilia. Niente libreria di optimistic state da mantenere, niente race condition.

03

L'hook use() semplifica i dati asincroni

Leggere una promise dentro un componente significava una boundary Suspense più un fetcher più un wrapper tipizzato. L'hook `use()` accetta direttamente la promise e Suspense gestisce il resto. Il livello dati si restringe; la boundary diventa la parte che davvero progetti.

04

Server Components, stabili, non sperimentali

Il Server Component gira sul server, il Client Component gira nel browser e il confine fra i due è un'annotazione chiara a livello di file. Il recupero dei dati avviene dove i dati vivono. Il bundle resta piccolo perché la maggior parte dei componenti non arriva mai al browser.

05

Document metadata, ref-as-prop, asset loading: piccole vittorie che fanno la somma

Title e meta tag nativi emessi da qualunque punto dell'albero, la cerimonia di `forwardRef` sparisce, la nuova API di asset loading precarica fogli di stile e script mentre la pagina si renderizza. Ognuna è piccola; messe insieme tolgono centinaia di righe di codice.

Cosa sviluppiamo con questa tecnologia

Architettura React 19 in produzione

Server Component di default, Client Component dove vive l'interazione, boundary `"use client"` a livello di file rivedute per l'impatto sul bundle.

Form con React 19 Actions

Ogni form è una Action tipizzata con progressive enhancement, gli error state emergono via useActionState, il feedback ottimistico via useOptimistic.

Pattern di UI ottimistica

useOptimistic collegato a Server Actions per feedback istantaneo, rollback su errore server, gestione dei dati stale al success.

Streaming via Suspense

I fetch lunghi vanno in stream al browser mentre si risolvono, il first paint resta sotto un secondo e la parte lenta si accende quando è pronta.

Asset loading e preinit

`preload`, `preinit` e la nuova API di asset loading sostituiscono la manipolazione ad-hoc di `<link>` nell'head.

Document metadata nativi

Title, meta e link tag per route emessi dall'albero dei componenti, niente libreria di head manager.

ref come prop (fine di forwardRef)

I nuovi componenti accettano `ref` come prop normale, i vecchi migrano in modo incrementale quando vengono toccati.

Pattern TypeScript 5+ strict

Tipi di Action, tipi di Server Action, tipi di optimistic update: tutti generati dagli stessi schemi Zod che il server usa.

Migrazione da React 18

Codebase aggiornato sul posto, comportamento conservato, le nuove API adottate in modo incrementale così il team impara toccando codice vero.

Valutazione del React Compiler

Lo accendiamo dove paga (form, liste, alberi di memo pesanti) e lo lasciamo spento dove aggiunge frizione.

Tuning del concurrent rendering

`useTransition` e `useDeferredValue` piazzati dove servono davvero (typeahead, filtri di liste grosse), non sparsi in modo difensivo.

Error boundary con le nuove API

La nuova API di error reporting, boundary recuperabili, stack trace solo in dev collegati alla pipeline di logging esistente.

Un form con React 19 Actions, useOptimistic e una Server Action

Un comment composer che si aggiorna all'istante, sincronizza al database via Server Action, fa emergere gli errori inline e fa rollback dello stato ottimistico in caso di fallimento. Niente form library, niente libreria di state-management, niente fetch wrapper.

La maggior parte dei walkthrough "React 19 features" ti mostra le API in isolamento. React 19 in produzione assomiglia al form qui sotto: una Action gestisce il submit, useOptimistic aggiorna la UI prima che il server risponda, la Server Action scrive nel database e l'error state emerge inline se qualcosa va storto. Niente form library, niente libreria di optimistic state, niente fetch wrapper.

1. Il componente form

Il form riceve una prop action. Dentro, useActionState contiene la risposta del server, useOptimistic tiene il commento in volo, useFormStatus riporta se il form sta inviando. Tutto nativo, tutto stabile.

// app/[lang]/posts/[id]/CommentComposer.tsx
'use client'

import { useActionState, useOptimistic } from 'react'
import { useFormStatus } from 'react-dom'
import { addComment, type ActionState } from './actions'

interface Comment {
  id: string
  body: string
  authorName: string
  createdAt: string
}

const INITIAL_STATE: ActionState = { error: null, comment: null }

export function CommentComposer({
  postId,
  initialComments,
}: {
  postId: string
  initialComments: Comment[]
}) {
  const [state, formAction] = useActionState(addComment, INITIAL_STATE)
  const [optimisticComments, addOptimistic] = useOptimistic(
    initialComments,
    (current, draft: Comment) => [draft, ...current],
  )

  async function clientAction(formData: FormData) {
    const body = formData.get('body')?.toString() ?? ''
    addOptimistic({
      id: 'pending',
      body,
      authorName: 'You',
      createdAt: new Date().toISOString(),
    })
    await formAction(formData)
  }

  return (
    <section>
      <form action={clientAction}>
        <input type="hidden" name="postId" value={postId} />
        <textarea name="body" required minLength={2} />
        <SubmitButton />
        {state.error ? (
          <p role="alert" className="ds-text-error">
            {state.error}
          </p>
        ) : null}
      </form>

      <ul>
        {optimisticComments.map((c) => (
          <li key={c.id}>
            <strong>{c.authorName}</strong>
            <p>{c.body}</p>
          </li>
        ))}
      </ul>
    </section>
  )
}

function SubmitButton() {
  const { pending } = useFormStatus()
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Posting…' : 'Post comment'}
    </button>
  )
}

2. La Server Action

L'action gira solo sul server. Lo schema Zod valida l'input, il client Supabase scrive la riga, la forma della risposta combacia con l'ActionState che il form si aspetta. La revalidation dice a Next.js di rifare il fetch della lista commenti alla prossima navigazione.

// app/[lang]/posts/[id]/actions.ts
'use server'

import { revalidatePath } from 'next/cache'
import { z } from 'zod'
import { createServerClient } from '@/lib/supabase/server'

const Input = z.object({
  postId: z.string().uuid(),
  body: z.string().min(2).max(2000),
})

export interface ActionState {
  error: string | null
  comment: {
    id: string
    body: string
    authorName: string
    createdAt: string
  } | null
}

export async function addComment(
  _previousState: ActionState,
  formData: FormData,
): Promise<ActionState> {
  const parsed = Input.safeParse({
    postId: formData.get('postId'),
    body: formData.get('body'),
  })
  if (!parsed.success) {
    return { error: 'Please write at least two characters.', comment: null }
  }

  const supabase = await createServerClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return { error: 'Please sign in to comment.', comment: null }

  const { data, error } = await supabase
    .from('comments')
    .insert({
      post_id: parsed.data.postId,
      author_id: user.id,
      body: parsed.data.body,
    })
    .select('id, body, created_at, profiles(display_name)')
    .single()

  if (error) return { error: 'Could not post the comment.', comment: null }

  revalidatePath(`/posts/${parsed.data.postId}`)

  return {
    error: null,
    comment: {
      id: data.id,
      body: data.body,
      authorName: data.profiles?.display_name ?? 'Anonymous',
      createdAt: data.created_at,
    },
  }
}

3. Il Server Component che lo ospita

Il Server Component legge i commenti esistenti (row-level security limita la query ai commenti che l'utente può vedere) e renderizza il CommentComposer client già con i dati.

// app/[lang]/posts/[id]/page.tsx
import { createServerClient } from '@/lib/supabase/server'
import { CommentComposer } from './CommentComposer'

interface Props {
  params: Promise<{ id: string }>
}

export default async function PostPage({ params }: Props) {
  const { id } = await params
  const supabase = await createServerClient()

  const { data: comments } = await supabase
    .from('comments')
    .select('id, body, created_at, profiles(display_name)')
    .eq('post_id', id)
    .order('created_at', { ascending: false })

  const initial = (comments ?? []).map((c) => ({
    id: c.id,
    body: c.body,
    authorName: c.profiles?.display_name ?? 'Anonymous',
    createdAt: c.created_at,
  }))

  return <CommentComposer postId={id} initialComments={initial} />
}

4. Cosa fa collassare questo

Una versione pre-React-19 di questa feature aveva bisogno di: una form library per stato e validazione, un fetcher per la POST, una libreria di optimistic state (o un reducer custom), una boundary Suspense a livello di pagina, uno step manuale di revalidation e una visualizzazione errori separata. Tutti e cinque i problemi stanno ora in tre file, tutti nativi, tutti type-checked, tutti rivisti dalla stessa persona senza coordinamento tra librerie.

Lo "stack React 19" non è una lista di nuove API da rincorrere. È uno stack di dipendenze che spariscono perché il framework finalmente le ha assorbite.

Domande frequenti

React 19 è abbastanza stabile per la produzione?

Sì. Abbiamo rilasciato in produzione React 19 RC e 19 stable su più app dal rilascio. I caveat noti (librerie di terze parti con peer dependency stantie, edge case dei Server Component) sono documentati nel nostro playbook interno e li gestiamo nel codice.

Conviene aggiornare da React 18 ora?

Dipende dal codebase. La maggior parte delle app React 18 trae beneficio dall'aggiornamento perché le nuove API sostituiscono uno stack di dipendenze; la migrazione dura uno-tre giorni per un codebase medio. Ti diciamo dritto se l'upgrade non vale ancora il lavoro.

Il React Compiler è pronto?

Abbastanza pronto per molti form, liste e alberi di memo pesanti, non pronto per ogni codebase. Facciamo benchmark con e senza compiler sulla tua applicazione prima di rilasciare; i guadagni sono concreti in alcuni posti e zero in altri.

Quando usate Server Components rispetto a Client Components?

Server di default. Client solo quando il componente legge API solo-browser (useState, useEffect, window, intersection observer, eccetera) o interagisce direttamente con input. Ogni boundary `'use client'` viene rivista per l'impatto sul bundle.

useOptimistic rispetto a Zustand o Redux per gli optimistic update?

useOptimistic per il caso semplice (un solo form, una sola mutazione di lista), Zustand o Redux solo quando lo stato vive davvero su più componenti e va persistito oltre una singola interazione utente. La maggior parte dei progetti che tocchiamo perde la libreria di state dopo l'upgrade a React 19.

TypeScript strict con React 19?

Sì, ogni tipo si restringe dall'input dell'Action, attraverso la firma della Server Action, fino alla riga del database. Usiamo Zod alla boundary così il controllo runtime e il tipo compile-time hanno la stessa forma.

Come gestite la migrazione da librerie che restano indietro su React 19?

O fissiamo la libreria e conviviamo con i warning di peer-dep React 18 (quasi sempre innocui), o sostituiamo la libreria con un'alternativa attivamente mantenuta. Il piano di migrazione segnala ogni libreria che resta indietro così il costo è visibile in anticipo.

Raccontaci cosa stai costruendo su React 19

Una call di scoping, un numero concreto nella prima risposta, niente recite da agenzia. Nuovi progetti e migrazioni React 18 entrambi benvenuti.