Stati di attesa e caricamento con useFormStatus
Disabiliti i pulsanti e mostri gli spinner durante l’invio usando l’hook useFormStatus nei figli del form.
Stati di attesa e caricamento con useFormStatus è una lezione Next.js 15 Fullstack (App Router + Server Actions) gratuita su CoddyKit. Questa è la lezione 2 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Next.js 15 Fullstack (App Router + Server Actions), e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Next.js 15 Fullstack (App Router + Server Actions) include 4 lezioni in totale.
Parti di questa lezione non sono ancora state tradotte e vengono mostrate in inglese.
Why Pending States Matter
When a user submits a form backed by a Server Action, there is a network round-trip while the action runs on the server. Without feedback the user may click Submit twice, creating duplicate records.
- Disable the submit button while the action is in flight.
- Show a spinner or
Saving...label so the UI feels responsive. - Prevent double submissions automatically.
Next.js 15 (built on React 19) gives us a dedicated hook for exactly this: useFormStatus.
Meet useFormStatus
useFormStatus is a React hook imported from react-dom. It reports the status of the nearest parent <form> element.
It returns an object with these fields:
pending—truewhile the form is submitting.data— theFormDatabeing sent.method— the HTTP method (getorpost).action— the function or URL passed to the form'sactionprop.
For loading UX, pending is the field you will reach for most.
import { useFormStatus } from "react-dom";
// Returns: { pending, data, method, action }
const { pending } = useFormStatus();The Golden Rule: Call It Inside a Child
The most important rule: useFormStatus must be called from a component rendered inside the <form>, not from the component that renders the <form> itself.
It reads the status of its parent form, so if you call it in the same component that contains the <form> tag, it has no parent form to track and pending stays false forever.
The standard pattern is to extract a small SubmitButton client component and place it between the form tags.
A Basic SubmitButton Component
Create a dedicated client component for the submit button. Because it uses a hook, it needs the "use client" directive.
- Read
pendingfromuseFormStatus(). - Bind it to the button's
disabledattribute. - Swap the label based on
pending.
"use client";
import { useFormStatus } from "react-dom";
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Saving..." : "Save"}
</button>
);
}Wiring It Into a Form
Now render the SubmitButton inside a form whose action is a Server Action. The button automatically knows about the parent form's status — no props need to be passed down.
The parent form can stay a Server Component; only the button is a client component.
import { SubmitButton } from "./submit-button";
import { createPost } from "./actions";
export default function NewPostForm() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="body" required />
<SubmitButton />
</form>
);
}The Server Action Side
The pending flag becomes true the moment the form is submitted and flips back to false when the Server Action resolves. Here is a typical action that takes time (DB write + revalidation).
Mark the file with "use server" so each exported function is callable as a Server Action.
"use server";
import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";
export async function createPost(formData: FormData) {
const title = String(formData.get("title"));
const body = String(formData.get("body"));
await db.post.create({ data: { title, body } });
revalidatePath("/posts");
}Adding a Spinner
Beyond a label swap, you often want a visual spinner. Conditionally render a spinner element when pending is true and keep the button disabled.
- Keep the button
disabledso it cannot be clicked again. - Use
aria-disabledor visually hidden text for accessibility.
"use client";
import { useFormStatus } from "react-dom";
import { Spinner } from "@/components/spinner";
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending} aria-disabled={pending}>
{pending && <Spinner />}
{pending ? "Submitting" : "Submit"}
</button>
);
}Disabling Other Fields Too
useFormStatus isn't limited to buttons. Any child component inside the form can read pending and react to it. A common pattern is disabling inputs while submitting so the user can't edit data mid-flight.
Just remember each such component must be a client component placed inside the form.
"use client";
import { useFormStatus } from "react-dom";
export function TitleField() {
const { pending } = useFormStatus();
return (
<input
name="title"
required
disabled={pending}
placeholder="Post title"
/>
);
}Inspecting the Submitted Data
The data field exposes the in-flight FormData. This lets you show an optimistic preview of what is being saved — for example echoing the title while the request runs.
Guard for null: data is only populated while pending is true.
"use client";
import { useFormStatus } from "react-dom";
export function PendingPreview() {
const { pending, data } = useFormStatus();
if (!pending) return null;
const title = data?.get("title")?.toString() ?? "";
return <p>Saving \u201c{title}\u201d...</p>;
}useFormStatus vs useTransition
Two hooks track pending UI, but they solve different problems:
useFormStatus— purpose-built for forms; reads the nearest parent<form>'s status. No state to manage, but the component must live inside the form.useTransition— general-purpose; you call the action insidestartTransitionand readisPending. Works for buttons not wrapped in a form (e.g. a delete button calling an action viaonClick).
For declarative <form action={...}> submissions, prefer useFormStatus.
Pure TypeScript: Modeling the Status
Here is a framework-free way to think about what useFormStatus returns. We model the status shape and a tiny state machine that mirrors how pending flips during a submission.
This runs in any TypeScript judge — no React or server needed.
type FormStatus = {
pending: boolean;
method: "get" | "post" | null;
};
function simulateSubmit(): FormStatus[] {
const timeline: FormStatus[] = [];
timeline.push({ pending: false, method: null }); // idle
timeline.push({ pending: true, method: "post" }); // submitting
timeline.push({ pending: false, method: null }); // resolved
return timeline;
}
for (const s of simulateSubmit()) {
console.log(`pending=${s.pending} method=${s.method}`);
}Quick Check
You add useFormStatus() directly inside the same component that renders the <form> tag and bind pending to the submit button. The button never disables. Why?
Recap
You learned how to give forms responsive feedback with useFormStatus:
- Import it from
react-domand readpending(plusdata,method,action). - Always call it inside a child rendered within the
<form>— typically a"use client"SubmitButton. - Bind
pendingtodisabledand swap labels or show a spinner to prevent double submissions. - The parent form can stay a Server Component; only the interactive child is a client component.
- For actions not wrapped in a
<form>, reach foruseTransitioninstead.
Impara TypeScript con un tutor IA — gratis
Scrivi ed esegui vero codice nel tuo browser, ricevi aiuto istantaneo da un tutor IA disponibile 24/7, e riprendi da dove hai lasciato sul web o nell'app.
- Corsi
- 22
- Lezioni
- 88
Domande Frequenti
La lezione «Stati di attesa e caricamento con useFormStatus» è gratuita?
Sì — il testo completo di «Stati di attesa e caricamento con useFormStatus» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Next.js 15 Fullstack (App Router + Server Actions), passa a CoddyKit PRO. Il corso Next.js 15 Fullstack (App Router + Server Actions) include 4 lezioni in totale.
Cosa imparerò in «Stati di attesa e caricamento con useFormStatus»?
Disabiliti i pulsanti e mostri gli spinner durante l’invio usando l’hook useFormStatus nei figli del form. Eserciti Next.js 15 Fullstack (App Router + Server Actions) con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare Next.js 15 Fullstack (App Router + Server Actions)?
Non è richiesta alcuna esperienza precedente. Next.js 15 Fullstack (App Router + Server Actions) su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 2 di 4.
Quanto tempo richiede la lezione «Stati di attesa e caricamento con useFormStatus»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione Next.js 15 Fullstack (App Router + Server Actions)?
Sì. Ogni lezione Next.js 15 Fullstack (App Router + Server Actions) include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Progressive enhancement con la prop Action di form
- Stati di attesa e caricamento con useFormStatus
- Errori di validazione a livello di campo con useActionState
- Feedback immediato con useOptimistic