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

useFormStatus를 활용한 대기 및 로딩 상태

폼 자식 요소 내부에서 useFormStatus 훅을 사용하여 제출 중 버튼을 비활성화하고 로딩 표시기를 보여 주는 방법을 배웁니다.

레슨 2/413개 단계

useFormStatus를 활용한 대기 및 로딩 상태은(는) CoddyKit의 무료 Next.js 15 Fullstack (App Router + Server Actions) 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 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 — 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.
무료로 시작

AI 튜터와 함께 TypeScript을(를) 배우세요 — 무료

브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.

코스
22
레슨
88

자주 묻는 질문

“useFormStatus를 활용한 대기 및 로딩 상태” 강의는 무료인가요?

네 — “useFormStatus를 활용한 대기 및 로딩 상태” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Next.js 15 Fullstack (App Router + Server Actions) 강의 전체를 잠금 해제할 수 있습니다. Next.js 15 Fullstack (App Router + Server Actions) 강의에는 총 4개의 강의가 포함되어 있습니다.

“useFormStatus를 활용한 대기 및 로딩 상태”에서 뭘 배우나요?

폼 자식 요소 내부에서 useFormStatus 훅을 사용하여 제출 중 버튼을 비활성화하고 로딩 표시기를 보여 주는 방법을 배웁니다. 브라우저에서 직접 실행하는 실습 코드로 Next.js 15 Fullstack (App Router + Server Actions)을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Next.js 15 Fullstack (App Router + Server Actions)을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Next.js 15 Fullstack (App Router + Server Actions)은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“useFormStatus를 활용한 대기 및 로딩 상태” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Next.js 15 Fullstack (App Router + Server Actions) 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Next.js 15 Fullstack (App Router + Server Actions) 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. form Action 속성을 활용한 점진적 향상
  2. useFormStatus를 활용한 대기 및 로딩 상태
  3. useActionState를 활용한 필드별 검증 오류
  4. useOptimistic으로 즉각적인 피드백 제공
← Next.js 15 Fullstack (App Router + Server Actions)(으)로 돌아가기