第128章:練習:useFormStatus で送信ボタンを「送信中...」に変える
useFormStatus を使って「送信中...」ボタンに変身させるフォームを一緒につくっていきます💌✨
1. この章のゴール 🎯
この章が終わるころには、こんなことができるようになります👇
-
フォームを送信すると…
- 送信ボタンの文字が 「送信」 → 「送信中...」 に変わる
- ボタンが 自動で
disabledになる
-
この「送信中かどうか」は
useFormStatusフックで勝手に教えてもらう 💡 -
親コンポーネントから「isPending」を props で渡さなくてもOK → 子コンポーネントが自分でフォームの状態を知るようになる
2. 全体イメージを図で見る 🧠✨
useActionState でフォーム送信をしつつ、
useFormStatus で「送信中かどうか」をボタン側が受け取る、という流れです。
useActionStateで「送信処理」と「結果の state 管理」をやる 🧪(React)useFormStatusで「フォームの今の状態(pending など)」を子コンポーネントから読む (React)
3. 今回つくるミニアプリの構成 🏗️
ファイルはシンプルにこんな感じにします:
src/App.tsx→ 画面全体。<ContactForm />を表示するだけsrc/ContactForm.tsx→ 実際のフォーム本体(useActionStateを使う)src/SubmitButton.tsx→useFormStatusを使って「送信中...」に変身するボタン
※ すでに Vite + React + TS (react-ts テンプレ) でプロジェクトがある前提です。
(npm create vite@latest → テンプレ react-ts のやつ)
4. SubmitButton を作る:useFormStatus の主役登場 ✨
まずは 「ボタンだけ」のコンポーネントを作って、
その中で useFormStatus を使います。
📄 src/SubmitButton.tsx
import { useFormStatus } from "react-dom";
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
aria-busy={pending}
className="submitButton"
>
{pending ? "送信中..." : "送信"}
</button>
);
}
ポイント解説 📝
-
useFormStatusはreact-domから import します (React) -
{ pending }を取り出して、disabled={pending}→ 送信中のときだけ押せないようにaria-busy={pending}→ スクリーンリーダー向けに「今忙しいよ」と伝える
-
pending ? "送信中..." : "送信"→ 送信中だけラベルを変える 💫
⚠️ 大事なルール
useFormStatus は 必ず <form> の中(子孫コンポーネント)で使う必要があります。
フォームの外で使うと、pending はずっと false のままです。(MintJams)
5. フォーム本体を作る:useActionState と仲良くする 🤝
次に、フォーム本体をつくります。
ここで useActionState を使って、
- 送信処理(Action関数)
- 送信結果のメッセージ
をまとめて管理します。
📄 src/ContactForm.tsx
import { useActionState } from "react";
import { SubmitButton } from "./SubmitButton";
const initialState = {
message: "",
};
async function submitContact(
previousState: typeof initialState,
formData: FormData
): Promise<typeof initialState> {
const name = (formData.get("name") ?? "") as string;
const body = (formData.get("body") ?? "") as string;
// サーバーに送っている風のダミー処理(1.5秒待つだけ)
await new Promise((resolve) => setTimeout(resolve, 1500));
if (!name || !body) {
return {
message: "お名前とメッセージは必須です 🥺",
};
}
return {
message: `送信ありがとう、${name} さん!📨`,
};
}
export function ContactForm() {
const [state, formAction] = useActionState(submitContact, initialState);
return (
<div className="contactForm">
<h1>ミニお問い合わせフォーム ✍️</h1>
<form action={formAction}>
<div className="field">
<label>
お名前:
<input name="name" type="text" placeholder="React 太郎" />
</label>
</div>
<div className="field">
<label>
メッセージ:
<textarea
name="body"
rows={4}
placeholder="何かひとことどうぞ 💬"
/>
</label>
</div>
{/* ここがポイント:フォームの中で SubmitButton を使う */}
<SubmitButton />
</form>
{state.message && (
<p className="resultMessage" aria-live="polite">
{state.message}
</p>
)}
</div>
);
}
ここでやっていること 🧐
-
initialState→ フォームの「結果メッセージ」の初期値 -
submitContact(previousState, formData)useActionState用の「Action関数」- 第1引数:前回の state(今回はあまり使ってない)
- 第2引数:フォームから送られてきた
FormData - 今回は ダミーで 1.5 秒
setTimeoutしているだけ → ここを将来、本物の API 叩く処理に変えればOK👌
-
const [state, formAction] = useActionState(submitContact, initialState);(React)state→ 最新の送信結果({ message: string })formAction→<form action={formAction}>に渡すための関数
-
<form action={formAction}>の 子要素として<SubmitButton />を置いているので、SubmitButtonの中のuseFormStatus()が、このフォームのpending状態をちゃんと拾ってくれます 🎉(MintJams)
6. App.tsx で表示する 👀
最後に、App.tsx からこのフォームを表示します。
📄 src/App.tsx
import { ContactForm } from "./ContactForm";
function App() {
return (
<div style={{ padding: "2rem" }}>
<ContactForm />
</div>
);
}
export default App;
これでブラウザを開いて
- 名前とメッセージを入力
- 「送信」ボタンをクリック
すると…
- ボタンが「送信中...」に変わる
- ボタンが押せなくなる
- 1.5 秒後にメッセージが下に表示される
…という動きになるはずです ✨
7. 見た目をちょっとだけ整える(オプション)💄
CSS は軽くでOKですが、例としてこんな感じ。
📄 src/index.css など
body {
font-family: system-ui, sans-serif;
background: #f9fafb;
}
.contactForm {
max-width: 480px;
margin: 0 auto;
padding: 1.5rem;
border-radius: 1rem;
background: #ffffff;
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
}
.contactForm h1 {
font-size: 1.4rem;
margin-bottom: 1rem;
}
.field {
margin-bottom: 1rem;
}
.field label {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.field input,
.field textarea {
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
border: 1px solid #d1d5db;
font-size: 0.9rem;
}
.submitButton {
margin-top: 0.5rem;
padding: 0.6rem 1.4rem;
border-radius: 999px;
border: none;
font-size: 0.95rem;
cursor: pointer;
background: linear-gradient(135deg, #3b82f6, #6366f1);
color: #ffffff;
font-weight: 600;
}
.submitButton[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
.resultMessage {
margin-top: 1rem;
font-size: 0.95rem;
color: #16a34a;
}
かわいいフォームになります🩵
8. よくあるハマりポイント 🔍
❌ 1. <form> の外で useFormStatus を使っている
// ダメな例(外側)
export function Page() {
const { pending } = useFormStatus(); // ← ここでは pending はずっと false...
return (
<form action={formAction}>
{/* ... */}
</form>
);
}
✅ 必ず <form> の子孫コンポーネントの中で使うこと!
export function Page() {
return (
<form action={formAction}>
{/* 中で呼ぶ */}
<SubmitButton />
</form>
);
}
❌ 2. type="submit" を忘れている
useFormStatus は フォーム送信に反応します。
ボタンの type が button のままだと送信されないので、必ず
<button type="submit">送信</button>
にしておきましょう ✅
❌ 3. pending を props でも渡して二重管理してしまう
useFormStatus の良さは「props のバケツリレーを減らせる」ことです。(manuelsanchezdev.com)
// こう書くともったいない
<SubmitButton isPending={pending} />
// この章ではこっちのスタイルを練習👇
<SubmitButton />
// 中で useFormStatus() を呼ぶ
9. ミニ課題 📝(余裕があれば)
ちょっとだけ手を動かして慣れてみましょう✨
-
ボタンのラベルをアレンジしてみる
- 例)
"送信"→"送信する" "送信中..."→"送信中です ⏳"など
- 例)
-
送信中だけ別のメッセージを表示してみる
- 例)フォームの下に
「送信中… 画面は閉じずにお待ちください🙏」
と出す(
useFormStatusを使う小さなコンポーネントを増やしてもOK)
- 例)フォームの下に
「送信中… 画面は閉じずにお待ちください🙏」
と出す(
-
「送信完了!」メッセージの色をエラーと成功で変えてみる
submitContactでエラーかどうかも一緒に返すようにして それに応じて CSS クラスを切り替える 🔴🟢
まとめ 🎀
この章では、
-
useFormStatusを使ってフォームの「送信中」状態を読み取る -
その状態を使って
- ボタンを「送信中...」に変える
- ボタンを自動で無効化する
-
しかも props の受け渡しなしで、 子コンポーネントが自分でフォームの状態を知る
という、React 19 の新しいフォームスタイルを練習しました💡(React)
次のフォーム開発では、「とりあえず useFormStatus で pending を使えるかな?」と考えてみてくださいね ✨
だいぶコードがスッキリして、フォームまわりが好きになってくるはずです😍