Next.js 15 Fullstack (App Router + Server Actions) · บทเรียน

สถานะรอดำเนินการและกำลังโหลดด้วย useFormStatus

ปิดปุ่มและแสดงตัวหมุนระหว่างส่งข้อมูล โดยใช้ฮุก useFormStatus ภายในองค์ประกอบลูกของฟอร์ม

บทเรียน 2 จาก 413 ขั้นตอน

สถานะรอดำเนินการและกำลังโหลดด้วย useFormStatus เป็นบทเรียน Next.js 15 Fullstack (App Router + Server Actions) ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน 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 — 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.
เริ่มต้นได้ฟรี

เรียนรู้ TypeScript ด้วย AI tutor — ฟรี

เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป

คอร์ส
22
บทเรียน
88

คำถามที่พบบ่อย

บทเรียน “สถานะรอดำเนินการและกำลังโหลดด้วย useFormStatus” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “สถานะรอดำเนินการและกำลังโหลดด้วย useFormStatus” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส 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 ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Next.js 15 Fullstack (App Router + Server Actions) หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Next.js 15 Fullstack (App Router + Server Actions) บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน

บทเรียน “สถานะรอดำเนินการและกำลังโหลดด้วย useFormStatus” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Next.js 15 Fullstack (App Router + Server Actions) นี้ได้ไหม

ได้ บทเรียน Next.js 15 Fullstack (App Router + Server Actions) ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การเพิ่มประสิทธิภาพแบบก้าวหน้าด้วยพร็อพ Action ของฟอร์ม
  2. สถานะรอดำเนินการและกำลังโหลดด้วย useFormStatus
  3. ข้อผิดพลาดการตรวจสอบระดับฟิลด์ด้วย useActionState
  4. การตอบสนองทันทีด้วย useOptimistic
← กลับไปที่ Next.js 15 Fullstack (App Router + Server Actions)