使用 useFormStatus 处理待提交与加载状态
在表单提交期间使用表单子组件中的 useFormStatus 钩子禁用按钮并显示加载指示器。
使用 useFormStatus 处理待提交与加载状态 是 CoddyKit 上的免费 Next.js 15 Fullstack (App Router + Server Actions) 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Next.js 15 Fullstack (App Router + Server Actions) 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Next.js 15 Fullstack (App Router + Server Actions) 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
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.
常见问题解答
「使用 useFormStatus 处理待提交与加载状态」课时是免费的吗?
是的 — 「使用 useFormStatus 处理待提交与加载状态」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Next.js 15 Fullstack (App Router + Server Actions) 课程的其余内容,请升级到 CoddyKit PRO。 Next.js 15 Fullstack (App Router + Server Actions) 课程共包含 4 节课。
「使用 useFormStatus 处理待提交与加载状态」这节课中我会学到什么?
在表单提交期间使用表单子组件中的 useFormStatus 钩子禁用按钮并显示加载指示器。 你通过在浏览器中直接运行的动手代码来练习 Next.js 15 Fullstack (App Router + Server Actions),全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Next.js 15 Fullstack (App Router + Server Actions) 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Next.js 15 Fullstack (App Router + Server Actions) 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「使用 useFormStatus 处理待提交与加载状态」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Next.js 15 Fullstack (App Router + Server Actions) 课中编写并运行代码吗?
能。每节 Next.js 15 Fullstack (App Router + Server Actions) 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 使用表单 Action 属性实现渐进式增强
- 使用 useFormStatus 处理待提交与加载状态
- 使用 useActionState 处理字段级验证错误
- 使用 useOptimistic 提供即时反馈