第127章:【v19フック】useFormStatus
「送信中かどうか」を子コンポーネント側からこっそりチェックしよう✨
1. この章でやること 🎯
この章のゴールはこんな感じです👇
useFormStatusフックってなにか分かる ✅- 「フォームの送信中かどうか」を 子コンポーネント側で知る 仕組みを理解する ✅
- 送信ボタンを「送信中…」に変えたり、押せなくしたりできるようになる ✅
React 19 では、<form action={...}> とセットで、useActionState や useFormStatus という「フォーム専用フック」が追加されました。useFormStatus はその中でも、**「送信ボタンをいい感じにする担当」**みたいな子です🥰(React)
2. useFormStatus ってどんな子?👀
公式の説明をめちゃくちゃざっくり言うと…
useFormStatusは、「一番近い<form>の送信状態を教えてくれるフック」だよ。(React)
useFormStatus() を呼ぶと、こんな情報を持ったオブジェクトが返ってきます👇(React)
pending: 今、送信中ならtrue(ここが一番よく使うやつ)data: 直近で送信されたFormData(入力内容をここから読める)method:"get"/"post"などフォームのメソッドaction: どのアクションが呼ばれているか
ポイントは、
子コンポーネントが、自分で
propsを受け取らなくてもフォームの状態を知れる
というところです。これが React 19 フォームの、新しい便利ポイント🔥(MintJams)
3. 「昔のやり方」と「今のやり方」を比べてみる 🧪
🐢 昔のやり方(props 地獄)
送信中フラグを子コンポーネントに伝えるために、こんな感じでやっていました:
type SubmitButtonProps = {
isSubmitting: boolean;
};
function SubmitButton(props: SubmitButtonProps) {
return (
<button type="submit" disabled={props.isSubmitting}>
{props.isSubmitting ? "送信中..." : "送信"}
</button>
);
}
export function Form() {
const [isSubmitting, setIsSubmitting] = useState(false);
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setIsSubmitting(true);
// 送信処理...
setIsSubmitting(false);
}
return (
<form onSubmit={handleSubmit}>
{/* フォーム項目いろいろ */}
<SubmitButton isSubmitting={isSubmitting} />
</form>
);
}
- 親が
isSubmittingをuseStateで持つ - 子に
isSubmittingをpropsで渡す - どんどんネストすると、バケツリレー状態…🥲
⚡ React 19 のやり方(useFormStatus)
useFormStatus を使うと、子コンポーネント側で「送信中かどうか」を直接聞けます👇(React)
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "送信中..." : "送信"}
</button>
);
}
export function Form() {
async function action(formData: FormData) {
// フォーム送信処理(サーバー通信など)
}
return (
<form action={action}>
{/* フォーム項目いろいろ */}
<SubmitButton />
</form>
);
}
- 親は「送信状態」を
stateで持たなくてOK ✨ - 子が
useFormStatus()で勝手にフォーム状態を読みにいってくれる ✨ - その結果、コンポーネント同士の「依存」が減って、部品として再利用しやすくなります🌸(Zenn)
4. 大事なルール:「フォームの中」にいないとダメ 🚫
useFormStatus には超大事なルールがひとつあります👇
useFormStatus()を使うコンポーネントは、必ず<form>の中で表示されていること。 そうでないと、pendingはずっとfalseのままです。(React)
OK な例↓
export function App() {
return (
<form action={someAction}>
{/* ✅ フォームの中 */}
<SubmitButton />
</form>
);
}
ダメな例↓
export function App() {
return (
<>
{/* ❌ フォームの外なので pending は動かない */}
<SubmitButton />
<form action={someAction}>
{/* ここに SubmitButton がいない */}
</form>
</>
);
}
イメージを図にしてみると…🧠
5. ミニアプリ:お問い合わせフォーム+送信ボタン ✉️
では実際に、「送信中…」ボタン付きお問い合わせフォームを作ってみます。
5-1. 送信ボタンコンポーネントを作る 💄
src/SubmitButton.tsx みたいなファイルを用意します。
import { useFormStatus } from "react-dom";
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
style={{
padding: "0.6rem 1.2rem",
borderRadius: "999px",
border: "none",
cursor: pending ? "not-allowed" : "pointer",
opacity: pending ? 0.7 : 1,
}}
>
{pending ? "送信中..." : "送信する"}
</button>
);
}
ここがポイント👇
-
const { pending } = useFormStatus();pending === trueのときだけボタンを無効化 (disabled)- 文言も「送信中…」にチェンジ🌀
5-2. フォーム本体を作る 📝
src/App.tsx にフォームを作ります。
import { SubmitButton } from "./SubmitButton";
async function contactAction(formData: FormData) {
// 疑似的な通信待ち(1.5秒待つ)
await new Promise((resolve) => {
setTimeout(resolve, 1500);
});
const name = formData.get("name");
const message = formData.get("message");
console.log("送信されたお問い合わせ", { name, message });
// 本当のアプリではここでサーバーに送信したり、DBに保存したりする
}
export function App() {
return (
<main
style={{
maxWidth: "480px",
margin: "2rem auto",
padding: "1.5rem",
borderRadius: "1rem",
border: "1px solid #eee",
fontFamily: "system-ui, sans-serif",
}}
>
<h1 style={{ fontSize: "1.5rem", marginBottom: "1rem" }}>
お問い合わせフォーム ✉️
</h1>
<form
action={contactAction}
style={{ display: "flex", flexDirection: "column", gap: "1rem" }}
>
<label style={{ display: "flex", flexDirection: "column", gap: "0.25rem" }}>
お名前
<input
name="name"
placeholder="例)山田 花子"
required
style={{ padding: "0.5rem", borderRadius: "0.5rem", border: "1px solid #ccc" }}
/>
</label>
<label style={{ display: "flex", flexDirection: "column", gap: "0.25rem" }}>
メッセージ
<textarea
name="message"
rows={4}
placeholder="ご質問やご相談をどうぞ♪"
required
style={{ padding: "0.5rem", borderRadius: "0.5rem", border: "1px solid #ccc" }}
/>
</label>
{/* ここが useFormStatus を使っている子コンポーネント */}
<SubmitButton />
</form>
</main>
);
}
contactActionがasync関数である間、React がフォームを「送信中」とみなしてくれます。その状態をuseFormStatusで読んでボタン表示を変えている、という流れです。(React)
6. data を使って「誰のデータが送信中か」も出してみる 🧃
useFormStatus が返す data には、直近の送信で送られた FormData が入ってきます。(React)
ちょっとだけ応用して、「○○さんのメッセージ送信中…」みたいな表示にしてみます。
import { useFormStatus } from "react-dom";
export function FancySubmitMessage() {
const { pending, data } = useFormStatus();
const nameRaw = data?.get("name");
const name =
typeof nameRaw === "string" && nameRaw.length > 0 ? nameRaw : "メッセージ";
if (!pending) {
return null;
}
return (
<p style={{ fontSize: "0.9rem", color: "#666" }}>
{name} を送信中です... ⏳
</p>
);
}
そしてフォーム側で👇
<form action={contactAction}>
{/* ...入力項目... */}
<FancySubmitMessage />
<SubmitButton />
</form>
pending === trueの間だけメッセージを出すdata?.get("name")を読んで、「誰のメッセージか」も表示- UX がちょっとリッチになりますね✨
7. useActionState とどう分担するの?💭
前の章で登場した useActionState と useFormStatus は、ざっくりこう分担すると分かりやすいです:(React)
-
useActionState- フォームの結果(エラー / 成功メッセージ / 返ってきたデータ) を扱う
- 親フォームコンポーネントでよく使う
-
useFormStatus- 「送信中かどうか」や、送信中のデータ を扱う
- 主に子コンポーネント(ボタンやスピナーなど)で使う
なので、
- エラーメッセージの表示 →
useActionState - ボタンの
disabledと「送信中…」表示 →useFormStatus
と覚えておくと、頭の中がスッキリします🧠✨
8. よくあるハマりポイント ⚠️
最後に、useFormStatus でハマりやすいポイントをまとめておきます。
-
フォームの外で使っている
- →
pendingが一生falseのままです - 必ず
<form>の中でレンダーされているコンポーネントで使うこと!(React)
- →
-
actionが同期処理で一瞬で終わる- →
pendingがほぼ見えないレベルで一瞬だけtrueになります - 通信など、ちゃんと
awaitがある処理で使うと分かりやすいです
- →
-
複数のフォームがあるページ
useFormStatusは「一番近い親の<form>」を見ます- 別のフォームと混ざらないように、コンポーネントの位置に注意👀
-
propsで状態を渡す癖が抜けない- React 19 のフォームでは、「
propsで渡さなくても勝手に分かる」パターンが増えました - それでも迷ったら、「親 = 結果管理 (
useActionState)、子 = 見た目管理 (useFormStatus)」を思い出してみてください💡
- React 19 のフォームでは、「
9. ちょこっと練習問題 🧩
自分の手でも試してみましょう〜!
- 今回のお問い合わせフォームに、チェックボックス(「注意事項に同意します」など)を追加してみる
- 送信中の間だけ、フォーム全体に薄いグレーの背景を敷く(
pendingによってopacityを変えるなど) data?.get("message")を使って、「このメッセージを送信中です」みたいなプレビューを出してみる
ここまでできれば、useFormStatus はバッチリです🎉
次の章では、これをさらに実践的な「送信ボタン UI 改造」に発展させていきますね💪💕