0Pricing
Next.js 15 Fullstack (App Router + Server Actions) · Aula

Estados pendente e de carregamento com useFormStatus

Desative botões e mostre indicadores de carregamento durante o envio usando o hook useFormStatus dentro dos filhos do formulário.

Estados pendente e de carregamento com useFormStatus é uma aula grátis de Next.js 15 Fullstack (App Router + Server Actions) no CoddyKit. Esta é a aula 2 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de Next.js 15 Fullstack (App Router + Server Actions), e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de Next.js 15 Fullstack (App Router + Server Actions) inclui 4 aulas no total.

Partes desta aula ainda não foram traduzidas e aparecem em inglês.

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.

Perguntas Frequentes

A aula “Estados pendente e de carregamento com useFormStatus” é grátis?

Sim — o texto completo de “Estados pendente e de carregamento com useFormStatus” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de Next.js 15 Fullstack (App Router + Server Actions), atualize para CoddyKit PRO. O curso de Next.js 15 Fullstack (App Router + Server Actions) inclui 4 aulas no total.

O que vou aprender em “Estados pendente e de carregamento com useFormStatus”?

Desative botões e mostre indicadores de carregamento durante o envio usando o hook useFormStatus dentro dos filhos do formulário. Você pratica Next.js 15 Fullstack (App Router + Server Actions) com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar Next.js 15 Fullstack (App Router + Server Actions)?

Nenhuma experiência prévia é necessária. Next.js 15 Fullstack (App Router + Server Actions) no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 2 de 4.

Quanto tempo leva a aula “Estados pendente e de carregamento com useFormStatus”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de Next.js 15 Fullstack (App Router + Server Actions)?

Sim. Cada aula de Next.js 15 Fullstack (App Router + Server Actions) inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. Aprimoramento progressivo com a propriedade action de formulários
  2. Estados pendente e de carregamento com useFormStatus
  3. Erros de validação por campo com useActionState
  4. Feedback instantâneo com useOptimistic
← Voltar para Next.js 15 Fullstack (App Router + Server Actions)