Next.js 15 Fullstack (App Router + Server Actions) · Lezione

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.

Lezione 2 di 413 passaggi

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 — true while the form is submitting.
  • data — the FormData being sent.
  • method — the HTTP method (get or post).
  • action — the function or URL passed to the form's action prop.

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 pending from useFormStatus().
  • Bind it to the button's disabled attribute.
  • 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 disabled so it cannot be clicked again.
  • Use aria-disabled or 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 inside startTransition and read isPending. Works for buttons not wrapped in a form (e.g. a delete button calling an action via onClick).

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-dom and read pending (plus data, method, action).
  • Always call it inside a child rendered within the <form> — typically a "use client" SubmitButton.
  • Bind pending to disabled and 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 for useTransition instead.
Gratis per iniziare

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

  1. Progressive enhancement con la prop Action di form
  2. Stati di attesa e caricamento con useFormStatus
  3. Errori di validazione a livello di campo con useActionState
  4. Feedback immediato con useOptimistic
← Torna a Next.js 15 Fullstack (App Router + Server Actions)