第123章:action に渡す非同期関数
この章では、React v19 の フォーム Actions の中でも、
「action に **非同期関数 (async function) を渡す」」ところだけにギュッとフォーカスしていきます 💌
🎯 この章のゴール
この章が終わるころには…
<form action={...}>に 関数を渡すしくみがイメージできる 🧠actionに渡す関数をasync/await付きで書ける ✍️- TypeScript での 型 (
FormData,Promise<void>) の付け方がわかる ✅ - 簡単な「お問い合わせフォーム」くらいなら、自分で書けちゃう 🌟
1️⃣ まずはイメージ:何が変わったの?🤔
React 19 では、<form> の action に **URL じゃなくて「関数」**を渡せるようになりました。
その関数には、フォームの値がぎゅっと詰まった FormData オブジェクトが引数として渡されます。(React)
ざっくり比べると、こういう感じです👇
🐢 昔ながらの書き方(イメージだけ見ればOK)
import React from "react";
function OldStyleForm() {
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault(); // 自分で止める
const formData = new FormData(e.currentTarget);
const name = formData.get("name");
console.log("名前:", name);
// ここで fetch してサーバーに送る…など
}
return (
<form onSubmit={handleSubmit}>
<input name="name" />
<button type="submit">送信</button>
</form>
);
}
onSubmitでイベント (e) を受け取るe.preventDefault()を自分で書くnew FormData(e.currentTarget)で FormData を作る
🚀 React 19 スタイル(この章の主役)
function NewStyleForm() {
function handleAction(formData: FormData) {
const name = formData.get("name");
console.log("名前:", name);
}
return (
<form action={handleAction}>
<input name="name" />
<button type="submit">送信</button>
</form>
);
}
actionに 関数を直接渡す- React がよしなに
FormDataをつくって、関数に渡してくれる preventDefault()とか もう書かなくてOK ✨
しかも、この handleAction は 非同期関数 (async) にしてもいいんです。
そのときにどう書くか、がこの章のテーマです 💪
2️⃣ 送信の流れを図で見る 🧠✨(Mermaid)
React がどんな順番で動いているか、かんたんなシーケンス図で見てみましょう。
ポイント 💡
- ユーザーが送信すると、React が 自動で FormData を作って action 関数を呼ぶ
- 関数が
asyncなら、Promise が解決(awaitが終わる)まで待ってくれる - 処理が成功すると、フォームの「非制御な入力欄」はリセットされる(React)
3️⃣ いちばんシンプルな「非同期 action」✉️
まずは、**「1秒待ってからアラートを出すだけ」**の超シンプルな例からスタートします。
export function SimpleAsyncForm() {
// 🔑 React 19 の <form> action 用の非同期関数
async function handleSubmit(formData: FormData): Promise<void> {
const name = formData.get("name");
if (typeof name !== "string" || name.trim() === "") {
alert("名前を入れてね 🥺");
return;
}
// 疑似的な「重い処理」🕐(API呼び出しの代わり)
await new Promise((resolve) => setTimeout(resolve, 1000));
alert(`こんにちは、${name} さん!🌸`);
}
return (
<form action={handleSubmit}>
<label>
お名前:
<input name="name" />
</label>
<button type="submit">送信する</button>
</form>
);
}
👀 見てほしいところ
-
関数の定義:
async function handleSubmit(formData: FormData): Promise<void> { ... }- 引数は 必ず
FormData一個だけ(eventじゃないよ!) asyncを付けると、Promise<void>を返す関数になる
- 引数は 必ず
-
FormData#getの戻り値はFormDataEntryValue | nullなので、 型安全にしたいならtypeof name === "string"でチェックすると安心 🔒
4️⃣ もう一歩:APIっぽい処理を書く 🛰️
次は、「お問い合わせ送信」っぽいフォームを書いてみます。
本物のサーバーの代わりに、ここではダミーの fakePostContact 関数に投げます。
type ContactPayload = {
name: string;
message: string;
};
async function fakePostContact(payload: ContactPayload): Promise<void> {
// 🕐 疑似的に 1.5 秒待つ(サーバー通信のつもり)
await new Promise((resolve) => setTimeout(resolve, 1500));
console.log("サーバーに送られたデータ:", payload);
// ここで実際は fetch(...) したり、API クライアントを呼んだりするイメージ
}
export function ContactForm() {
async function sendContact(formData: FormData): Promise<void> {
const name = formData.get("name");
const message = formData.get("message");
if (typeof name !== "string" || typeof message !== "string") {
alert("フォームの値の取得に失敗しました 🙇♀️");
return;
}
if (name.trim() === "" || message.trim() === "") {
alert("名前とメッセージの両方を書いてね ✍️");
return;
}
await fakePostContact({ name, message });
alert("お問い合わせを送信しました!📨 ありがとう〜");
}
return (
<form action={sendContact}>
<div>
<label>
お名前:
<input name="name" />
</label>
</div>
<div>
<label>
メッセージ:
<textarea name="message" rows={4} />
</label>
</div>
<button type="submit">送信する</button>
</form>
);
}
🌟 ここで学んでいること
actionに渡す関数をasyncにして、 中で **awaitを使って非同期処理(API通信など)**を書けるようにするFormDataから値を取り出したあと、 型と中身をちょっとチェックしてから使う(堅めだけど安心)- 非同期処理が終わると、React がフォームの入力欄をリセットしてくれる(非制御の場合)(React)
5️⃣ TypeScript 的な「型」もしっかり押さえよう 📘
action に渡す関数の型は、React 公式の <form> ドキュメント的にはざっくりこんなイメージです:(React)
action?: string | ((formData: FormData) => void | Promise<void>)
つまり、非同期にしたいときは Promise<void> を返す関数を書けばOKです。
✅ 型をちゃんと書くバージョン
type FormAction = (formData: FormData) => Promise<void>;
const sendContact: FormAction = async (formData) => {
const name = formData.get("name");
const message = formData.get("message");
if (typeof name !== "string" || typeof message !== "string") {
alert("フォームの値の取得に失敗しました 🙇♀️");
return;
}
await fakePostContact({ name, message });
};
- こんな感じで 型エイリアス
FormActionを作っておくと、 別ファイルでも統一した書き方ができてスッキリします ✨
(後の章で登場する useActionState や useFormStatus も、
この「Action 関数」の型をベースに動いています。(React))
6️⃣ よくあるつまずきポイント 🥲 Q & A
Q1. つい event を引数に書いちゃう…
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }
👉 React 19 の <form action={...}> では、
引数は FormData だけです!
function handleSubmit(formData: FormData) { ... }
event.preventDefault()は React がやってくれるので不要です 🙅♀️
Q2. await を書いたらエラーになった…
function handleSubmit(formData: FormData) {
const res = await fetch(...); // ← ここが怒られる
}
👉 関数の先頭に async を付けるのを忘れているパターンです。
async function handleSubmit(formData: FormData): Promise<void> {
const res = await fetch("/api/contact", {
method: "POST",
body: formData,
});
}
Q3. formData.get(...) の型が string | File | null になってて気持ち悪い…
FormData#get の戻り値は FormDataEntryValue | null です。
文字列だけを想定している場合は、こうやってチェックするのが無難です ✅
const value = formData.get("title");
if (typeof value !== "string" || value.trim() === "") {
// エラー処理
return;
}
// ここから value は string 型として扱える
7️⃣ 手を動かす練習ミッション 🎮
自分の環境で試してみると、一気に理解が深まります ✨
📝 ミッション1:メールアドレス付きお問い合わせ
name,email,messageの 3 つの項目を持つフォームを作るactionに非同期関数を渡して、fakePostContactにまとめて送るemailが空のときはalert("メールアドレスも入れてね")を出して return する
🛜 ミッション2:実際に fetch を使ってみる(余裕があれば)
https://httpbin.org/postなどのテスト用エンドポイントにfetchしてみるmethod: "POST",body: formDataで投げて、console.logでレスポンスを見る
🎨 ミッション3:送信中っぽい演出を入れてみる
sendContactの中で 2 秒くらいawaitする- その間に
console.log("送信中...")を出してみる - 後の章で
useFormStatusを使うと、送信ボタンを「送信中...」にできるようになります 💫
8️⃣ この章のまとめ 🌈
- React 19 では
<form>のactionに 関数(Action) を渡せる action関数はFormDataを 1 つだけ受け取り、voidまたはPromise<void>を返す(非同期ならPromise<void>)(React)async function handle(formData: FormData)と書けば、 中でawait fetch(...)などの非同期処理を安全に書けるFormData#getの戻り値はstring確定ではないので、typeof === "string"で絞ってから使うと TypeScript 的にも安心 🔒
次の章(第124章)では、
ここで書いた 非同期 action 関数を使って、
実際に「フォーム → アクション」の流れを完成させていきます 💌✨
おつかれさま〜!ここまでできれば、React 19 のフォーム革命の入り口はバッチリです 🎉