← 解説「useActionState — フォームの結果と送信中を表示する」に戻る
サンプルコードで身につける: useActionState — フォームの結果と送信中を表示する
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1最小の useActionState
Server Action の戻り値を画面に表示する最小形です。action が返した文字列が result に入って再描画される、という「サーバーの結果が状態になる」流れをまず押さえましょう。
"use client";
import { useActionState } from "react";
import { sendMessage } from "./actions"; // 結果の文字列を返す Server Action
export default function MessageForm() {
const [result, formAction] = useActionState(sendMessage, null);
return (
<form action={formAction}>
<input name="message" />
<button type="submit">送信</button>
{result && <p>{result}</p>} {/* action の戻り値が表示される */}
</form>
);
}
2送信中はボタンを無効化する
3つ目の戻り値 isPending を使って、送信中の二重クリックを防ぎつつ「送信中...」と表示する例です。連打による二重送信はフォームの定番事故で、この数行がその対策の基本形になります。
"use client";
import { useActionState } from "react";
import { sendMessage } from "./actions";
export default function MessageForm() {
const [result, formAction, isPending] = useActionState(sendMessage, null);
return (
<form action={formAction}>
<textarea name="message" />
<button disabled={isPending}>
{isPending ? "送信中..." : "送信"}
</button>
{result && <p>{result}</p>}
</form>
);
}
// disabled={isPending} の1属性で二重送信を防げる。
3(前回の結果, formData) の2引数
useActionState 経由で呼ばれる Server Action は、第1引数に前回の結果、第2引数に FormData を受け取ります。レッスン本文の注意点である「FormData は2番目」を、action 側のコードで確認する例です。
// app/contact/actions.ts
"use server";
// useActionState 経由では (前回の結果, formData) の2引数になる
export async function sendMessage(
prevResult: string | null, // 1番目: 前回 action が返した値
formData: FormData // 2番目: 今回のフォーム入力
): Promise<string> {
const message = String(formData.get("message") ?? "");
if (message.trim() === "") {
return "メッセージを入力してください";
}
return "送信しました: " + message;
}
// formData を1番目だと思い込むと get できずに悩むことになる。
// 「前回の結果が先、FormData が後」と覚える。
4状態が更新される流れを再現する
useActionState の「action の戻り値が次の状態になる」という更新サイクルを、プレーンな関数の繰り返し呼び出しで再現する例です。前回の状態を引き継ぎながら結果を上書きしていく感覚を、実行して確かめられます。
TypeScript
5ニュースレター登録フォーム
成功・失敗をオブジェクトの状態で返し、エラー表示・成功メッセージ・送信中の無効化まで揃えた実務水準のフォームです。useActionState の3つの戻り値が、フォーム UX に必要な要素にそれぞれ対応していることが分かります。
"use client";
import { useActionState } from "react";
import { subscribe } from "./actions";
// subscribe は { ok: boolean; text: string } を返す Server Action
type SubscribeState = { ok: boolean; text: string } | null;
export default function NewsletterForm() {
const [state, formAction, isPending] = useActionState<SubscribeState, FormData>(
subscribe,
null
);
return (
<form action={formAction}>
<input name="email" placeholder="メールアドレス" />
<button disabled={isPending}>
{isPending ? "登録中..." : "登録する"}
</button>
{state && (
<p style={{ color: state.ok ? "green" : "red" }}>{state.text}</p>
)}
</form>
);
}