useActionState deep dive: flussi di mutation, UI ottimistica, coda di Actions e quando un data layer vince ancora (React 19+)
Un deep dive pratico su useActionState in React 19: come funziona la coda delle Actions, perché le Transitions sono cruciali per isPending, progressive enhancement con permalink, integrazione di useOptimistic per UX immediata, cancellazione con AbortController e un framework decisionale su quando TanStack Query / SWR / RTK Query devono ancora possedere caching, retry e invalidazione.

Guida React 2026 Primitives & Compiler Upgrade
Questo ? un capitolo della React 2026 Primitives & Compiler Upgrade Guide. Focus: useActionState ? flussi di mutation, optimistic UX, semantica della coda e pattern di integrazione.
Tutti gli articoli di questa guida
01
Panoramica: React 2026 primitives e mental model dell?era compiler
Il quadro architetturale completo: cosa ? cambiato e dove ogni primitive trova davvero posto.
02
useActionState in profondit?: mutation flows, optimistic UI e integration patterns
Quando useActionState riduce il boilerplate e quando un data layer resta il vero owner del problema.
03
React <Activity />: mantieni lo state, metti in pausa gli Effects e renderizza in background
Pattern reali, trade-off di performance e pitfalls per tabs, drawers e shell UI.
04
useEffectEvent in profondit?: design degli Effect, subscriptions e analytics
Confini degli Effect compatibili con il linter senza stale closures o reconnect churn.
cosa otterrai
Questo capitolo riguarda correttezza, UX e confini di ownership: cosa garantisce useActionState, cosa non garantisce e come usarlo senza reinventare un data layer incompleto.
isPending dipende dalle Transitions e i due modi sicuri per triggerare actions. [2]AbortController quando l’utente può “spammare” actions. [2]<Activity /> (state retention + comportamento degli Effects + pitfall di rollout). [13]La maggior parte delle app ha due responsabilità principali: mostrare dati e modificare dati. Il loading spesso è semplice; la complessità vive nelle mutation — correttezza, UX, debug e gestione degli stati.
La direzione “Actions” in React 19 punta a standardizzare l’handshake delle mutation: pending, optimistic UI e gestione errori senza macchine a stati ad-hoc per ogni form o bottone. [1]
useActionState è uno strato sottile ma fondamentale: incapsula un’Action, memorizza l’ultimo risultato come state ed espone isPending per mantenere la UI reattiva durante l’async. In più, include una semantica chiave: le dispatch sono in coda ed eseguite in modo sequenziale. [2]
Se un team deve ricordare una sola cosa, dovrebbe essere questo flusso. Molta confusione sparisce quando l'ownership è esplicita.
L'intento dell'utente entra tramite un'Action
Un submit di form o una dispatch manuale avvia il flusso di mutation. Se chiami il dispatcher a mano, deve essere dentro startTransition. [2]
React traccia pending e mette in coda le dispatch successive
Ogni nuova dispatch aspetta la precedente, e ogni esecuzione riceve l'ultimo risultato completato come previousState. [2]
La tua action restituisce stato UI, non una cache
Usa lo state restituito per messaggi, errori di campo, id e UX post-submit locale. Non trasformarlo di nascosto in storage condiviso.
In breve
Tratta useActionState come il sottile guscio UI attorno a una mutation, non come system of record.
La signature è simile a un reducer: l’action riceve prima previousState, poi il payload (per le form spesso FormData). Il valore di ritorno diventa il prossimo state. [2]
React mette in coda più dispatch e le esegue in sequenza. Ogni run vede il risultato precedente come previousState. È una feature di correttezza, ma significa anche backpressure se l’utente triggera actions più velocemente di quanto completino. [2]
Le Transitions non sono opzionali. Se chiami manualmente il dispatcher, deve essere dentro una Transition (startTransition). Se lo passi a action / formAction, React lo avvolge per te. Altrimenti isPending può comportarsi male e React può avvisare. [2]
Error handling: se l’action lancia, React può saltare le dispatch in coda successive. Per failure attese (validazione, 4xx) è meglio restituire uno state di errore invece di fare throw. Riserva il throw a casi eccezionali gestiti da error boundary. [2]
Progressive enhancement è nel design: permalink è per framework RSC-capable così la form funziona prima che il JS sia caricato. Con Server Functions, anche l’initial state deve essere serializzabile. [2]
Questi sono gli errori che fanno sembrare useActionState peggiore di quanto sia. L'API è piccola; l'uso scorretto è il problema tipico.
Chiamare il dispatcher fuori da startTransition e poi chiedersi perché isPending è fuorviante. [2]
Fare throw per errori attesi (validazione, 4xx) invece di restituire uno stato di errore strutturato. [2]
Infilare payload grandi nello state dell'action e trasformare l'hook UI in una pseudo-cache.
In breve
La maggior parte dei problemi di rollout viene dal mescolare responsabilità, non dall'hook in sé.
Il caso d’uso che “vince subito” sono le form: errori di validazione lato server, field errors e pending state che disabilita submit senza prop drilling. I suoi esempi ufficiali lo trattano come target primario. [1][2][4]
Un pattern pratico: modellare lo state come discriminated union, così la UI non deve mai “indovinare” cosa contiene.
import { useActionState } from "react";
type FieldErrors = { email?: string; password?: string };
type SubmitState =
| { kind: "idle" }
| { kind: "error"; message: string; fieldErrors?: FieldErrors }
| { kind: "success"; message: string };
async function register(
_prev: SubmitState,
formData: FormData,
): Promise<SubmitState> {
const email = String(formData.get("email") ?? "").trim();
const password = String(formData.get("password") ?? "");
const fieldErrors: FieldErrors = {};
if (!email.includes("@")) fieldErrors.email = "Enter a valid email.";
if (password.length < 8) fieldErrors.password = "Use at least 8 characters.";
if (fieldErrors.email || fieldErrors.password) {
return { kind: "error", message: "Fix the highlighted fields.", fieldErrors };
}
const res = await fetch("/api/register", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
return { kind: "error", message: "Registration failed. Try again." };
}
return { kind: "success", message: "Account created." };
}
export function RegisterForm() {
const [state, action, isPending] = useActionState<SubmitState, FormData>(
register,
{ kind: "idle" },
);
return (
<form action={action} className="space-y-3">
<div className="space-y-1">
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" />
{state.kind === "error" && state.fieldErrors?.email ? (
<p role="alert">{state.fieldErrors.email}</p>
) : null}
</div>
<div className="space-y-1">
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
{state.kind === "error" && state.fieldErrors?.password ? (
<p role="alert">{state.fieldErrors.password}</p>
) : null}
</div>
<button type="submit" disabled={isPending}>
{isPending ? "Creating…" : "Create account"}
</button>
{state.kind === "error" ? <p role="alert">{state.message}</p> : null}
{state.kind === "success" ? <p>{state.message}</p> : null}
</form>
);
}Un dettaglio importante: le form Actions in React 19 possono resettare gli input uncontrolled dopo un submit riuscito, e React espone requestFormReset per reset manuali. Per flussi “salva bozza” conviene testare uncontrolled vs controlled con attenzione. [1]
Se hai una component library (Button, SubmitButton, FormFooter), passare isPending attraverso i livelli è noioso. useFormStatus legge lo status della form padre come un context ed è pensato esattamente per questo. [4]
import { useFormStatus } from "react-dom";
type Props = { label: string };
export function SubmitButton({ label }: Props) {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Submitting…" : label}
</button>
);
}React sottolinea una capacità quando useActionState è usato con Server Functions: i framework possono mostrare risposte server e mantenere le form interattive prima (progressive enhancement / prima che l’hydration finisca). permalink serve perché un submit pre-hydration sappia dove navigare. [2]
In Next.js, queste sono Server Functions (Server Actions nel contesto mutation): async sul server invocate via rete. Next evidenzia l’avvolgimento in Transition e il modello di caching (un roundtrip può restituire UI aggiornata e nuovi dati). [6]
Passare il dispatcher a <form action={dispatchAction}> avvolge il submit in una Transition automaticamente; React descrive anche progressive enhancement con Server Functions e permalink. [2]
Next.js: le Server Actions si integrano con l’architettura di caching; <form action> / formAction sono avvolte in startTransition automaticamente e le actions usano POST. [6]
Optimistic UI non significa “far finta che sia veloce”. Significa mostrare lo stato finale atteso mentre la request è pending e poter recuperare bene in caso di errore. useOptimistic in React 19 è progettato per lavorare con le Actions: lo stato ottimistico vive durante l’Action e poi React torna allo state source-of-truth quando l’action finisce o fallisce. [1][3]
Un trucco importante dai docs: con useOptimistic(items, reducer), React può rieseguire il reducer se la prop base items cambia mentre l’action è pending. Questo riduce bug di optimistic state stantio in UI collaborative o quasi real-time. [3]
import { useActionState, useOptimistic } from "react";
type Todo = { id: string; title: string; pending?: true };
type AddState = { lastError: string | null };
type ApiResult =
| { ok: true; created: { id: string; title: string } }
| { ok: false; error: string };
async function addTodoOnServer(title: string): Promise<ApiResult> {
const res = await fetch("/api/todos", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ title }),
});
if (!res.ok) return { ok: false, error: "Failed to create todo." };
const data: unknown = await res.json();
const created = data as { id?: unknown; title?: unknown };
if (typeof created.id !== "string" || typeof created.title !== "string") {
return { ok: false, error: "Bad response." };
}
return { ok: true, created: { id: created.id, title: created.title } };
}
function optimisticReducer(
state: Todo[],
action:
| { type: "add"; todo: Todo }
| { type: "replace"; tempId: string; real: Todo },
) {
if (action.type === "add") return [action.todo, ...state];
return state.map(t => (t.id === action.tempId ? action.real : t));
}
export function TodoComposer({ todos }: { todos: Todo[] }) {
const [optimisticTodos, applyOptimistic] = useOptimistic(todos, optimisticReducer);
const [state, submit, isPending] = useActionState<AddState, FormData>(
async (prev, formData) => {
const title = String(formData.get("title") ?? "").trim();
if (!title) return { lastError: "Title is required." };
const tempId = `temp:${Date.now()}`;
applyOptimistic({ type: "add", todo: { id: tempId, title, pending: true } });
const result = await addTodoOnServer(title);
if (!result.ok) return { lastError: result.error };
applyOptimistic({
type: "replace",
tempId,
real: { id: result.created.id, title: result.created.title },
});
return { lastError: null };
},
{ lastError: null },
);
return (
<div className="space-y-3">
<form action={submit} className="flex gap-2">
<input name="title" placeholder="Add a todo…" />
<button type="submit" disabled={isPending}>Add</button>
</form>
{state.lastError ? <p role="alert">{state.lastError}</p> : null}
<ul className="space-y-1">
{optimisticTodos.map(t => (
<li key={t.id}>
{t.title}{t.pending ? " (saving…)" : ""}
</li>
))}
</ul>
</div>
);
}La coda delle Actions è un dettaglio spesso sottovalutato. La dispatch sequenziale evita una classe di race (risposte tardive che sovrascrivono intenti più recenti). Ma può anche creare backlog: lo spam di click serializza chiamate di rete e rende la UI lenta. React consiglia esplicitamente useOptimistic, cancellazione o un approccio diverso per casi complessi. [2][3]
La cancellazione è utile quando vuoi “latest intent wins”, ma ha trade-off di correttezza: abortire una request non riavvolge una mutation sul server. È più sicuro quando l’effetto è idempotente, ignorabile in sicurezza o retryable. [2]
import { startTransition, useActionState, useRef } from "react";
type State = { value: number; error: string | null };
type Payload = { delta: number };
async function postDelta(delta: number, signal: AbortSignal): Promise<number> {
const res = await fetch("/api/counter", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ delta }),
signal,
});
if (!res.ok) throw new Error("Request failed");
const data: unknown = await res.json();
const next = (data as { value?: unknown }).value;
if (typeof next !== "number") throw new Error("Bad response");
return next;
}
export function Stepper() {
const abortRef = useRef<AbortController | null>(null);
const [state, dispatch, isPending] = useActionState<State, Payload>(
async (prev, payload) => {
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
try {
const nextValue = await postDelta(payload.delta, controller.signal);
return { value: nextValue, error: null };
} catch (e) {
const message = e instanceof Error ? e.message : "Unknown error";
return { value: prev.value, error: message };
}
},
{ value: 0, error: null },
);
function bump(delta: number) {
startTransition(() => dispatch({ delta }));
}
return (
<div className="space-y-2">
<div>Value: {state.value}{isPending ? " (updating…)" : ""}</div>
<div className="flex gap-2">
<button onClick={() => bump(-1)}>-</button>
<button onClick={() => bump(1)}>+</button>
</div>
{state.error ? <p role="alert">{state.error}</p> : null}
</div>
);
}React docs: AbortController può cancellare Actions pending, ma abortire una request non annulla le mutation lato server. [2]
Screenshot della sezione queued-actions-and-cancellationIl confine pulito: useActionState è ottimo per l’handshake UI-owned della mutation; i data layer eccellono nella correttezza di sistema (cache, retry, invalidazione, sync cross-screen).
React 19 offre un primitivo migliore per le mutation, ma non prova a sostituire le responsabilità delle data library. Devi comunque decidere chi possiede caching e sincronizzazione. [1][2]
Una tabella decisionale semplice (heuristica, non dogma):
| Esigenza | useActionState | Data layer |
|---|---|---|
| Pending + field/server errors nelle form | ✅ | ✅ |
| Mutation one-off con update UI locale | ✅ | ✅ |
| Caching + dedupe + background refetch | ❌ | ✅ (SWR / TanStack Query) |
| Policy di retry / backoff | ❌ | ✅ |
| Invalidazione cache su molte schermate | ❌ | ✅ |
| Offline queue / persistence | ❌ | ✅ |
| Optimistic UI in ambito locale | ✅ (con useOptimistic) | ✅ |
| Optimistic UI + riconciliazione con cache condivisa | ⚠️ | ✅ |Se la mutation impatta più query o schermate, spesso vuoi che la library che modella quel grafo possieda invalidazione e refetch. TanStack Query tratta l’invalidation come workflow di base; RTK Query fa lo stesso con i tag; SWR ha primitive di mutation + revalidation per motivi simili. [7][9][8]
Dove useActionState brilla
Dove un data layer vince
| Comparison point | useActionState va bene | Data layer va bene |
|---|---|---|
| Pending di form + errori campo/server | Sì | Sì |
| Mutation semplice one-off con update UI locale | Sì | Sì |
| Caching + dedupe + background refetch | No | Sì (SWR / TanStack Query) |
| Policy di retry / backoff | No | Sì |
| Invalidazione cache su molte schermate | No | Sì |
| Code offline / persistenza | No | Sì |
| Optimistic UI per scope locale piccolo | Sì (con useOptimistic) | Sì |
| Optimistic UI + riconciliazione cache condivisa | Rischioso | Sì |
Un ibrido pratico: usa useActionState per success/error a livello form e TanStack Query per invalidare così le altre schermate riflettono la mutation. I docs mostrano invalidateQueries come step standard e notano che restituire una Promise in onSuccess mantiene la mutation pending finché l’invalidation non completa. [7]
import { useActionState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
type State = { ok: boolean; message: string };
type Payload = { title: string };
declare function addTodo(input: Payload): Promise<void>;
export function AddTodo() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: addTodo,
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
const [state, dispatch, isPending] = useActionState<State, FormData>(
async (_prev, formData) => {
const title = String(formData.get("title") ?? "").trim();
if (!title) return { ok: false, message: "Title is required." };
try {
await mutation.mutateAsync({ title });
return { ok: true, message: "Saved." };
} catch {
return { ok: false, message: "Failed to save." };
}
},
{ ok: true, message: "" },
);
return (
<form action={dispatch} className="flex gap-2">
<input name="title" placeholder="New todo" />
<button type="submit" disabled={isPending}>
{isPending ? "Saving…" : "Save"}
</button>
{state.message ? <span>{state.message}</span> : null}
</form>
);
}TanStack Query: invalidation dopo mutation via onSuccess + invalidateQueries (Promise.all per più chiavi). [7]
Screenshot della sezione integration-example-react-queryLa maggior parte delle regressioni nasce da responsabilità mischiate o semantiche ignorate: transitions, queueing, reset e dimensione dello state.
Heuristica pratica: nello state dell’action conserva solo ciò che serve alla UI post-mutation (errori, success, id). Dati duraturi o condivisi devono vivere su server, cache del framework o data layer. [1][6][7]
obbligatoria
La dispatch manuale deve essere dentro startTransition, altrimenti React avvisa e isPending non si comporta correttamente. action/formAction avvolge per te. [2]
attenzione allo spam
La dispatch sequenziale evita alcune race, ma può serializzare interazioni in un backlog. Per rapid-fire usa useOptimistic, cancellazione o un modello diverso. [2][3]
tenetelo piccolo
Tratta l’action state come UI state (errori, messaggi, id), non come cache. Oggetti grandi aumentano il costo di render e complicano la riconciliazione.
testate uncontrolled
Le form Actions possono resettare gli input uncontrolled dopo successo; requestFormReset esiste per reset manuali. Flussi bozza/progresso richiedono una scelta intenzionale. [1]
Se adotti useActionState in tutta la codebase, questi controlli evitano le trappole più comuni di correttezza e UX.
• Modella il risultato dell’action come discriminated union (evita state ambigui).
• Per failure attese restituisci uno state di errore invece di fare throw, così non salti le dispatch in coda. [2]
• La dispatch manuale deve stare in
startTransition, altrimentiisPendingè fuorviante. [2]• Per interazioni “spammy” scegli: consistenza sequenziale, cancellazione (AbortController) o optimistic reducer. [2][3]
• Se la mutation impatta più schermate, delega invalidation/refetch al data layer (o al cache model di Next). [7][9][6]
• Con Server Functions, assicurati che l’initial state sia serializzabile e testa progressive enhancement (
permalink/ submit pre-hydration). [2][6]• Testa i reset degli input uncontrolled e decidi se servono controlled inputs o reset esplicito. [1]
No. useActionState è ottimo per l’handshake UI (pending + result + errors). I data layer possiedono ancora caching, retry, invalidazione, background refetch e sincronizzazione cross-screen. Se la mutation impatta più query/route, un data layer (o il cache model del framework come Next.js) è di solito il proprietario giusto. [7][8][9][6]
Se chiami manualmente il dispatcher, deve essere dentro una Transition (`startTransition`). In alternativa, passalo come `action`/`formAction` e React lo avvolge automaticamente. Altrimenti React può avvisare e `isPending` può risultare scorretto. [2]
Sì: React mette le dispatch in coda e le esegue in sequenza, quindi ogni esecuzione vede il risultato precedente come `previousState`. Questo evita alcune bug out-of-order, ma può creare backpressure in percorsi “spammy”. [2]
Usa `useOptimistic` per UI immediata, cancella lavoro pending con `AbortController` quando vuoi “latest intent wins”, o valuta un modello diverso se useActionState non è il primitivo adatto. I docs di React indicano esplicitamente queste escape hatch. [2][3]
Per failure attese (validazione, 4xx) restituisci uno state di errore invece di fare throw. Se l’action lancia, React può saltare le dispatch in coda successive. Riserva il throw a casi eccezionali gestiti da error boundary. [2]
`permalink` è pensato per framework RSC-capable con progressive enhancement. Se una form viene inviata prima che il JS carichi, il browser può navigare al permalink route invece di dipendere dall’URL corrente. Spesso il framework gestisce questo pattern, ma conta se vuoi “funziona prima del JS”. [2][6]
Le form Actions in React 19 possono resettare automaticamente gli input uncontrolled dopo successo. React fornisce anche `requestFormReset` per reset manuali. Nei flussi bozza/progresso conviene scegliere intenzionalmente uncontrolled vs controlled e testare il comportamento. [1]
Includiamo solo fonti che supportano direttamente semantica, caveat e integrazioni citate in questo capitolo.
• 4. useFormStatus (official reference): reading parent form status without prop drilling
• 8. SWR (official): Mutation & Revalidation (useSWRMutation + cache update behavior)
• 9. Redux Toolkit (official): RTK Query Automated Re-fetching (cache tags invalidation model)
• 13. PAS7 Studio (published chapter): React <Activity /> deep dive (series next chapter reference)
useActionState è potente quando il confine resta pulito: la UI possiede l’handshake della mutation, il sistema possiede la correttezza dei dati condivisi.
Se stai facendo upgrade a React 19 (o introducendo Server Actions, optimistic UX e una strategia di caching), PAS7 Studio può fare un audit dei flussi di mutation, definire confini di ownership e supportare un rollout incrementale con guardrail.
useActionState in profondit?: mutation flows, optimistic UI e integration patterns
Sei qui: 02/04
useActionState in profondit?: mutation flows, optimistic UI e integration patterns
Articoli correlati
AI SEO / GEO nel 2026: i tuoi prossimi clienti non sono umani — sono agenti
La ricerca sta passando dai click alle risposte. Bot e agenti AI scansionano, citano, raccomandano e sempre più spesso acquistano. Scopri cosa significa AI SEO / GEO, perché la SEO classica non basta più e come PAS7 Studio aiuta i brand a vincere visibilità nel web “agentico”.
Il chip Apple più potente? M5 Pro e M5 Max battono i record
Analisi di Apple M5 Pro e M5 Max aggiornata a marzo 2026. Spieghiamo perché questi chip possono essere considerati i SoC professionali per notebook più potenti di Apple, come si posizionano contro M4 Pro, M4 Max, M1 Pro, M1 Max e cosa mostrano rispetto ai concorrenti Intel e AMD.
Tag automatici e ricerca per link salvati
Integra con GDrive/S3/Notion per tag automatici e ricerca veloce tramite API di ricerca
Sviluppo di bot e servizi di automazione
Sviluppo professionale di bot Telegram e automazione dei processi aziendali: chatbot, assistenti AI, integrazioni CRM, automazione dei flussi di lavoro.
Sviluppo professionale per la tua attività
Creiamo soluzioni web moderne e bot per le aziende. Scopri come possiamo aiutarti a raggiungere i tuoi obiettivi.