React
Form progressivo in Next.js: funziona senza JavaScript
Una Server Action in action invia il modulo anche prima dell'idratazione: il browser fa un POST nativo, useActionState aggiunge solo lo stato di errore.
In sintesi
- Un Server Action passato ad `action` produce un `
- La validazione vera sta nello schema Zod sul server: gli attributi HTML sono solo comodità.
- Senza JavaScript non c'è stato di attesa: la protezione dal doppio invio deve stare sul server.
Risposta breve: passa la Server Action alla prop action del form e valida sul server. Next.js rende un modulo HTML con l'identificativo dell'azione in un campo nascosto, quindi il POST parte anche prima dell'idratazione; useActionState aggiunge solo lo stato di errore.
L'azione, con la validazione dentro
"use server";
import { z } from "zod";
import { redirect } from "next/navigation";
const schema = z.object({
email: z.email(),
message: z.string().min(20, "Almeno 20 caratteri."),
});
export async function subscribe(_prev: unknown, formData: FormData) {
const parsed = schema.safeParse(Object.fromEntries(formData));
if (!parsed.success) {
const first = parsed.error.issues[0];
return { error: first.message };
}
await saveLead(parsed.data);
redirect("/contatti/grazie");
}
Il modulo, con lo stato in più
"use client";
import { useActionState } from "react";
import { subscribe } from "./actions";
export default function ContactPage() {
const [state, action, pending] = useActionState(subscribe, null);
return (
{state?.error && {state.error}}
Iscrivimi
);
}
curl -s http://localhost:3000/contatti | grep -o 'ACTION_ID_[0-9a-f]*'
Il comando stampa l'identificativo se il modulo è progressivo: quel campo nascosto dice al browser dove inviare il POST quando React non è ancora partito. Se non stampa niente, il form non ha una Server Action ma un gestore onSubmit, e senza JavaScript non fa nulla.
Cosa cambia senza JavaScript
| Fase | Con JavaScript | Senza JavaScript |
|---|---|---|
| Invio | La Server Action parte dal client | POST nativo del browser |
| Validazione fallita | Errore mostrato senza ricaricare | Pagina nuova con l'errore dentro |
| Stato di attesa | Bottone disabilitato da pending | Nessuno: resta l'indicatore del browser |
| Redirect finale | Navigazione client | Risposta del server con la nuova pagina |
Le regole che non si vedono
- Gli attributi
requiredeminLengthsono cortesia verso l'utente: chiunque può inviare il POST senza di loro. - Lo schema Zod vive in un file
"use server"e non importa nulla dal client. redirectva chiamato fuori da untry/catch: lancia un errore interno che il framework deve intercettare.
Fonte: la documentazione Next.js sulle Server Action e sui form (licenza MIT per il codice degli esempi) e il criterio WCAG 2.2 3.3.1 sugli errori identificati a testo; useActionState è l'hook stabile di React 19 che sostituisce useFormState.
Limite dichiarato
Senza JavaScript non esiste lo stato di attesa: il bottone non si disabilita e un doppio click invia due POST. La protezione va sul server, con una chiave di idempotenza unica per invio, perché la disabilitazione è solo interfaccia. Secondo limite: la pagina è client per usare useActionState, quindi il modulo non è un server component puro; il POST resta nativo, ma il markup del form lo disegna il client dopo l'idratazione.
Dove continuare
Nel catalogo trovi la libreria shadcn, un blocco form con anteprima e la categoria form. La struttura del progetto è in struttura di un progetto App Router e la validazione è in validare una Server Action; il confronto tra le due strade è Route Handler contro Server Action. Per i campi dentro il modulo leggi select con ricerca e calendar e date picker.
Domande frequenti
Un form con Server Action funziona davvero senza JavaScript?
Sì. Next.js rende il modulo con l'identificativo dell'azione in un campo nascosto, quindi il browser invia un POST nativo e riceve la pagina nuova.
Perché l'errore non compare senza JavaScript?
Perché `useActionState` è un hook client. Senza JavaScript il server risponde con la pagina renderizzata di nuovo, e l'errore deve essere nel markup restituito, non solo nello stato del client.
Posso tenere la pagina come server component?
Sì, spostando `useActionState` in un componente client figlio che avvolge il modulo. Il server component disegna la pagina, il client gestisce lo stato.
Dove va la validazione, in HTML o in Zod?
In Zod. Gli attributi HTML evitano un viaggio inutile ma non fermano nessuno: chi invia il POST a mano li salta senza problemi.
Perché il redirect dentro un try/catch rompe tutto?
Perché `redirect` segnala la navigazione lanciando un errore speciale. Un `catch` lo intercetta e la funzione restituisce il valore sbagliato, quindi la pagina non si muove.