ReactやNext.js(App Router)でフォームを作ろうとすると、最近必ず見かけるようになった最新フック useActionState。
ネットのサンプルコードを見ると、なんだか引数や型がたくさん並んでいて「呪文みたいで難しそう…」と圧倒されていませんか?
かつての私もそうでした。
しかし、このフックは仕組みをバラバラに分解してみると、驚くほど理にかなったシンプルな作りをしています。
この記事では、「なぜこの引数が必要なの?」「データの流れはどうなってるの?」という疑問を、1つずつ丁寧に紐解いていきます!
Contents
1. そもそも useActionState ってなに?(旧 useFormState)
一言でいうと、useActionState は「『useState』と『フォーム送信』が合体して進化したフック」です。
💡 調べるときに混乱しないためのプチ知識
このフックは、少し前まで useFormState という名前で呼ばれていました。
Reactの最新バージョン(React 19)へのパワーアップに伴い、より機能が広がって useActionState という名前に生まれ変わったという歴史があります。
ネットの記事や少し古めの教材では useFormState と書かれていることも多いですが、役割や使い方はほぼ同じなので、「あ、改名したんだな」と思って読み進めて大丈夫です!
おなじみの useState を思い浮かべてください。
あちらは const [state, setState] = useState(初期値) という形で、手動で次の状態をセットしていましたよね。
useActionState は、その「次の状態をセットする」という仕事を、フォーム送信(非同期処理)と完全に自動で連動させてくれる道具なのです。
💡 イメージの核心
「useState の state が第1引数として関数に引き継がれ、第2引数にはフォームの入力データ(FormData)が自動で放り込まれる仕組み」 これが useActionState の正体です。
2. 題材にするコード
今回は、よくある「記事の投稿フォーム」を例に解説します。
// ① stateの中身(今回はエラーか、成功か、または空っぽか)を状態管理
type ActionResult = {
error?: string;
success?: boolean;
} | null;
export default function CreatePostForm() {
// ② ここが主役のフック!
const [state, formAction, isPending] = useActionState<ActionResult, FormData>(
createPost,
null,
);
return (
<form action={formAction}>
<h2>新しい記事を投稿</h2>
{/* ③ 成功・エラーの条件付き表示 */}
{state?.success && <p>✅ 記事を投稿しました!</p>}
{state?.error && <p>❌ {state.error}</p>}
<input name="title" placeholder="タイトル" />
<textarea name="body" placeholder="本文" />
{/* ④ 送信中のボタン制御 */}
<button type="submit" disabled={isPending}>
{isPending ? "投稿中..." : "投稿する"}
</button>
</form>
);
}
「うわ、やっぱり難しそう…」と思った方も大丈夫。
ここから4つのパーツにバキバキに分解して解説します!
3. パーツ分解①:[state, formAction, isPending] は「席順」が命!
まず、フックの左側のこの部分。
const [state, formAction, isPending] = useActionState(...);
これはJavaScriptの「配列の分割代入」という文法です。
実は、この変数名は元々決まっているわけではなく、自分で自由に変えてもOKです(例えば [result, action, loading] などでも動きます)。
ただし、「受け取る順番(席)」だけは厳格に決まっています。
- 1番目の席(
state):現在の処理結果(状態管理するべきデータ)が入る箱。 - 2番目の席(
formAction):<form action={formAction}>に渡して使う、送信時のスイッチ。 - 3番目の席(
isPending):現在送信中か(true/false)を教えてくれるフラグ。
💡 知っておくと便利な応用テクニック
「送信中の文字切り替え(isPending)は使わないからいらないや」という時は、
後ろをバッサリ省略して2つだけ受け取ることができます。
// 後ろを書かなければ、1番目と2番目だけが手に入る
const [state, formAction] = useActionState(createPost, null);
逆に、「結果(state)はいらないけど、送信中フラグだけ欲しい」ときは、カンマだけ置いて席を飛ばすことも可能です。
// 1番目を空っぽにしてスキップ!
const [, formAction, isPending] = useActionState(createPost, null);
4. パーツ分解②:引数
useActionState(createPost, null);
- 第一引数:createPost・・・フォームのデータを具体的に処理する関数(あらかじめ別ファイルなどで定義しておく)
- 第二引数:null・・・「最初の状態(ボタンを押す前の state の中身)
第一引数のデータを処理する関数
formActionをトリガーにuseActionStateによって関数を呼び出されます。その時、フォームのデータ(FormData)も引数として渡されます。
createPost関数の定義の例は以下を参考にしてください。
type ActionResult = {
error?: string;
success?: boolean;
} | null;
export async function createPost(
prevState: ActionResult,
formData: FormData
): Promise<ActionResult> {
const title = formData.get('title') as string;
const body = formData.get('body') as string;
if (!title || !body) {
return { error: 'タイトルと本文は必須です' };
}
// 実際のアプリではここでDBに保存する
return { success: true };
}今回は関数名をcreatePostとしましたが、名前に決まりはないです。分かりやすい名前で定義してください。
useActionStateの引数にアロー関数で直接渡しても良いです。(分けた方が読みやすい)
第二引数の null ってなに?
「最初の状態(ボタンを押す前の state の中身)」を指定する場所です。
今回のフォーム、ページを開いた直後のまっさらな状態では、まだエラーも成功メッセージも画面に出したくないですよね。
だから、データが何もない状態を表す null を初期値として置いているのです。
useStateの初期値と考えるとイメージが湧くでしょうか?
このデータはcreatePostの第一引数に渡されるデータです。
2回目以降は、前回のstate。 つまり前回returnで返した状態データが次の引数として渡されるイメージ
5. パーツ分解③:TypeScriptの型パズル
useActionState<ActionResult, FormData>(createPost, null);
この <ActionResult, FormData> という部分は、TypeScriptの型指定(ジェネリクス)です。
- 1つ目の型(ActionResult): Stateの型。フォームの処理結果やエラーメッセージの構造。(createPostの返り値)
- 2つ目の型(FormData): Payloadの型。Action関数(createPost)が受け取るデータの型。
6. 時系列で追う!データのバトンリレー
ここまでのパーツを踏まえて、実際にユーザーがボタンを押してから画面が変わるまでの流れを、タイムラインで追ってみましょう。
ここを理解すると、すべての謎が解けます!
ステップ1:ページを開いた直後(初期状態)
stateは、初期値に指定したnullです。- 画面のメッセージ部分は
{state?.success && ...}(もしstateがあったら表示する)という安全弁(?.)がついているので、nullのときは何も表示されません。 isPendingもfalse(送信中ではない)なので、ボタンの文字は「投稿する」です。
ステップ2:ボタンを押した瞬間(送信中)
- ユーザーが入力してボタンを押すと、
formActionが起動します。 - Reactが自動的に
isPendingをtrue(送信中)に切り替えます。 - 画面側では
disabled={isPending}が効いてボタンがグレーアウトし、ユーザーの連打を自動で防止してくれます。
ステップ3:裏側で行われる「データの引き渡し」
ここが一番の注目ポイントです! formAction が起動したとき、Reactは裏側で以下のようにデータを集めて、関数(createPost)を実行しています。
- ① フォームに入力されたデータを集めて
FormDataオブジェクト を作ります。 - ② 今持っている現在の状態(最初は初期値の
null、2回目以降はActionResult型の state)を用意します。 - ③ この2つのデータを両手に抱えて、
createPost(現在のstate, FormData)の順番で関数を実行します。
7. パーツ分解④:最大の謎「なぜ使わない引数(_)があるの?」
ステップ3のデータの動きを見たあとに、裏側で動く createPost の関数の形を見てみましょう。
// 呼び出される関数の形
async function createPost(prevState: ActionResult, formData: FormData) { ... }
「画面側では prevState(前回の状態)なんて一回も使ってないのに、なんで引数に書かなきゃいけないの?」という疑問の答えが、もう分かりますよね。
Reactが自動的に「第1引数に現在のstate(ActionResult型)」「第2引数にフォームデータ(FormData型)」という順番でデータを送りつけてくるからです。
私たちが本当に欲しいのは、フォームに入力されたデータ(第2引数の formData)です。
しかし、JavaScriptのルール上、「2番目の引数だけを受け取って、1番目を無視する(書かない)」ということはできません。
// ❌ 1番目を省略して、いきなり formData だけを受け取ることはできない
async function createPost(formData: FormData) { ... }
// ⭕ 1番目の席を空けておくことで、2番目で無事に formData が受け取れる
async function createPost(prevState: ActionResult, formData: FormData) { ... }
Reactが送ってくるデータの順番に合わせて、受け取る側も「席」を用意しておかないと、データの位置がズレて中身がめちゃくちゃになってしまうのです。
💡 使わないなら _(アンダースコア)にしよう
「ルール上、席は用意しなきゃいけないけれど、関数の中身では一切使わないよ」というとき、プログラミングの世界では変数名を _(アンダースコア単体) にしてしまうのが定番のテクニックです。
// 「第1引数はシステムの都合上置いてあるだけで、使いません」という合図
async function createPost(_, formData: FormData) {
const title = formData.get("title"); // これで無事に2番目を受け取れる!
}
まとめ
useActionState を初めて見ると、「おまじない」が多くて難しく感じてしまいます。
しかし、その正体は、
useStateのように状態(結果・送信中フラグ)を管理しながら- 自動でフォームのデータ(
FormData)を関数に届けてくれる
という、現代のWeb開発において超強力でスマートな仕組みです。
「時系列のデータの流れ」と「名前ではなく席順が命」というルールさえ押さえておけば、もうどんなフォームコードが出てきても怖くありませんよ!ぜひ自分のプロジェクトでも使ってみてください。


























