デザインについての学習メモブログ

ReactのuseActionState(旧 useFormState)とは?文法と考え方をわかりやすく解説

記事内に広告が含まれています。

ReactのuseActionState(旧 useFormState)とは?文法と考え方をわかりやすく解説

ReactやNext.js(App Router)でフォームを作ろうとすると、最近必ず見かけるようになった最新フック useActionState

ネットのサンプルコードを見ると、なんだか引数や型がたくさん並んでいて「呪文みたいで難しそう…」と圧倒されていませんか?

かつての私もそうでした。

しかし、このフックは仕組みをバラバラに分解してみると、驚くほど理にかなったシンプルな作りをしています。

この記事では、「なぜこの引数が必要なの?」「データの流れはどうなってるの?」という疑問を、1つずつ丁寧に紐解いていきます!

1. そもそも useActionState ってなに?(旧 useFormState

一言でいうと、useActionState は「『useState』と『フォーム送信』が合体して進化したフック」です。

💡 調べるときに混乱しないためのプチ知識

このフックは、少し前まで useFormState という名前で呼ばれていました。

Reactの最新バージョン(React 19)へのパワーアップに伴い、より機能が広がって useActionState という名前に生まれ変わったという歴史があります。

ネットの記事や少し古めの教材では useFormState と書かれていることも多いですが、役割や使い方はほぼ同じなので、「あ、改名したんだな」と思って読み進めて大丈夫です!

おなじみの useState を思い浮かべてください。

あちらは const [state, setState] = useState(初期値) という形で、手動で次の状態をセットしていましたよね。

useActionState は、その「次の状態をセットする」という仕事を、フォーム送信(非同期処理)と完全に自動で連動させてくれる道具なのです。

💡 イメージの核心

useStatestate第1引数として関数に引き継がれ、第2引数にはフォームの入力データ(FormData)が自動で放り込まれる仕組み」 これが useActionState の正体です。

2. 題材にするコード

今回は、よくある「記事の投稿フォーム」を例に解説します。

TypeScript
// ① 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] は「席順」が命!

まず、フックの左側のこの部分。

TypeScript
const [state, formAction, isPending] = useActionState(...);

これはJavaScriptの「配列の分割代入」という文法です。

実は、この変数名は元々決まっているわけではなく、自分で自由に変えてもOKです(例えば [result, action, loading] などでも動きます)。

ただし、「受け取る順番(席)」だけは厳格に決まっています。

  • 1番目の席(state:現在の処理結果(状態管理するべきデータ)が入る箱。
  • 2番目の席(formAction<form action={formAction}> に渡して使う、送信時のスイッチ。
  • 3番目の席(isPending:現在送信中か(true/false)を教えてくれるフラグ。

💡 知っておくと便利な応用テクニック

「送信中の文字切り替え(isPending)は使わないからいらないや」という時は、
後ろをバッサリ省略して2つだけ受け取ることができます。

TypeScript
// 後ろを書かなければ、1番目と2番目だけが手に入る
const [state, formAction] = useActionState(createPost, null);

逆に、「結果(state)はいらないけど、送信中フラグだけ欲しい」ときは、カンマだけ置いて席を飛ばすことも可能です。

TypeScript
// 1番目を空っぽにしてスキップ!
const [, formAction, isPending] = useActionState(createPost, null);

4. パーツ分解②:引数

TypeScript
useActionState(createPost, null);
  • 第一引数:createPost・・・フォームのデータを具体的に処理する関数(あらかじめ別ファイルなどで定義しておく)
  • 第二引数:null・・・「最初の状態(ボタンを押す前の state の中身)

第一引数のデータを処理する関数

formActionをトリガーにuseActionStateによって関数を呼び出されます。その時、フォームのデータ(FormData)も引数として渡されます。

createPost関数の定義の例は以下を参考にしてください。

TypeScript
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の型パズル

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 のときは何も表示されません。
  • isPendingfalse(送信中ではない)なので、ボタンの文字は「投稿する」です。

ステップ2:ボタンを押した瞬間(送信中)

  • ユーザーが入力してボタンを押すと、formAction が起動します。
  • Reactが自動的に isPendingtrue(送信中)に切り替えます。
  • 画面側では disabled={isPending} が効いてボタンがグレーアウトし、ユーザーの連打を自動で防止してくれます。

ステップ3:裏側で行われる「データの引き渡し」

ここが一番の注目ポイントです! formAction が起動したとき、Reactは裏側で以下のようにデータを集めて、関数(createPost)を実行しています。

  • ① フォームに入力されたデータを集めて FormData オブジェクト を作ります。
  • ② 今持っている現在の状態(最初は初期値の null、2回目以降は ActionResult 型の state)を用意します。
  • ③ この2つのデータを両手に抱えて、createPost(現在のstate, FormData) の順番で関数を実行します。

7. パーツ分解④:最大の謎「なぜ使わない引数(_)があるの?」

ステップ3のデータの動きを見たあとに、裏側で動く createPost の関数の形を見てみましょう。

TypeScript
// 呼び出される関数の形
async function createPost(prevState: ActionResult, formData: FormData) { ... }

「画面側では prevState(前回の状態)なんて一回も使ってないのに、なんで引数に書かなきゃいけないの?」という疑問の答えが、もう分かりますよね。

Reactが自動的に「第1引数に現在のstate(ActionResult型)」「第2引数にフォームデータ(FormData型)」という順番でデータを送りつけてくるからです。

私たちが本当に欲しいのは、フォームに入力されたデータ(第2引数の formData)です。

しかし、JavaScriptのルール上、「2番目の引数だけを受け取って、1番目を無視する(書かない)」ということはできません。

TypeScript
// ❌ 1番目を省略して、いきなり formData だけを受け取ることはできない
async function createPost(formData: FormData) { ... } 

// ⭕ 1番目の席を空けておくことで、2番目で無事に formData が受け取れる
async function createPost(prevState: ActionResult, formData: FormData) { ... }

Reactが送ってくるデータの順番に合わせて、受け取る側も「席」を用意しておかないと、データの位置がズレて中身がめちゃくちゃになってしまうのです。

💡 使わないなら _(アンダースコア)にしよう

「ルール上、席は用意しなきゃいけないけれど、関数の中身では一切使わないよ」というとき、プログラミングの世界では変数名を _(アンダースコア単体) にしてしまうのが定番のテクニックです。

TypeScript
// 「第1引数はシステムの都合上置いてあるだけで、使いません」という合図
async function createPost(_, formData: FormData) {
  const title = formData.get("title"); // これで無事に2番目を受け取れる!
}

まとめ

useActionState を初めて見ると、「おまじない」が多くて難しく感じてしまいます。

しかし、その正体は、

  1. useState のように状態(結果・送信中フラグ)を管理しながら
  2. 自動でフォームのデータ(FormData)を関数に届けてくれる

という、現代のWeb開発において超強力でスマートな仕組みです。

「時系列のデータの流れ」と「名前ではなく席順が命」というルールさえ押さえておけば、もうどんなフォームコードが出てきても怖くありませんよ!ぜひ自分のプロジェクトでも使ってみてください。