États d’attente et de chargement avec useFormStatus
Désactivez les boutons et affichez des indicateurs pendant l’envoi à l’aide du hook useFormStatus dans les éléments enfants du formulaire.
États d’attente et de chargement avec useFormStatus est une leçon Next.js 15 Fullstack (App Router + Server Actions) gratuite sur CoddyKit. Ceci est la leçon 2 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage Next.js 15 Fullstack (App Router + Server Actions), et ta progression se synchronise sur le web et l'application CoddyKit. Le cours Next.js 15 Fullstack (App Router + Server Actions) comprend 4 leçons au total.
Certaines parties de cette leçon n'ont pas encore été traduites et s'affichent en anglais.
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.
Apprends TypeScript avec un tuteur IA — gratuit
Écris et exécute du vrai code dans ton navigateur, obtiens de l'aide instantanée d'un tuteur IA disponible 24h/24, et reprends là où tu t'es arrêté sur le web ou dans l'app.
- Cours
- 22
- Leçons
- 88
Questions Fréquemment Posées
La leçon « États d’attente et de chargement avec useFormStatus » est-elle gratuite ?
Oui — le texte complet de « États d’attente et de chargement avec useFormStatus » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours Next.js 15 Fullstack (App Router + Server Actions), passe à CoddyKit PRO. Le cours Next.js 15 Fullstack (App Router + Server Actions) comprend 4 leçons au total.
Qu'est-ce que j'apprendrai dans « États d’attente et de chargement avec useFormStatus » ?
Désactivez les boutons et affichez des indicateurs pendant l’envoi à l’aide du hook useFormStatus dans les éléments enfants du formulaire. Tu pratiques Next.js 15 Fullstack (App Router + Server Actions) avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.
Dois-je avoir de l'expérience pour commencer Next.js 15 Fullstack (App Router + Server Actions) ?
Aucune expérience préalable n'est requise. Next.js 15 Fullstack (App Router + Server Actions) sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 2 sur 4.
Combien de temps prend la leçon « États d’attente et de chargement avec useFormStatus » ?
La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.
Peux-tu écrire et exécuter du code dans cette leçon Next.js 15 Fullstack (App Router + Server Actions) ?
Oui. Chaque leçon Next.js 15 Fullstack (App Router + Server Actions) inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.
Toutes les leçons de ce cours
- Amélioration progressive avec la propriété Action des formulaires
- États d’attente et de chargement avec useFormStatus
- Erreurs de validation au niveau des champs avec useActionState
- Retour instantané avec useOptimistic