# A progressive form in Next.js: it works without JavaScript

A Server Action on action={...} submits before hydration: the browser sends a native POST and useActionState only adds the error state on top.

> Page: https://ui.eneawork.it/blog/nextjs-form-progressivo
> Markdown: https://ui.eneawork.it/blog/nextjs-form-progressivo.md

*2026-11-01*

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.

Next.js è alla versione 16.4.3 e zod alla 4.6.7 nel registro npm, verificate il 1 novembre 2026. Il catalogo EneUI misura 111.985 componenti, 1.556 blocchi e 12.808 icone da 1.472 librerie, letti da /api/stats il 1 novembre 2026. Il comando di verifica qui sotto controlla che l'HTML contenga l'identificativo dell'azione.

## 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

## Le regole che non si vedono

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.

## FAQ

### 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.
