useActionState 完全入門【React】フォームアクションの状態管理

ReactのuseActionStateとは何か、フォーム送信後の状態(成功・エラー・ペンディング)を管理する方法を解説。React 19以降で追加されたサーバーアクションとの組み合わせ方も紹介します。

#react#hooks#useactionstate#javascript#frontend

フォーム送信後の「送信中」「成功」「エラー」という状態管理は、すべてのWebアプリで共通の課題です。useActionState はこのフォームのアクション結果に基づいた状態管理を、シンプルに実装するためのフックです。

React 19 から追加されたこのフックを使うと、JavaScriptが読み込まれる前でもフォームが機能する(Progressive Enhancement)実装が実現できます。

この記事でわかること:

  • useActionState が解決する問題
  • 3つのパラメータと戻り値の意味
  • フォーム送信の実装例
  • サーバーアクションとの組み合わせ(React 19+)

useActionState とは?

useActionState は、フォームのアクション結果に基づいて state を更新するフックです。

const [state, formAction, isPending] = useActionState(fn, initialState, permalink?);
パラメータ説明
fnフォーム送信時に呼ばれる関数。第1引数は前の state、第2引数以降は通常のフォームデータ
initialState初期状態として使う任意のシリアライズ可能な値
permalink(省略可能)Progressive Enhancement 用のユニークなURL
戻り値説明
state現在の state(初回は initialState
formAction<form action> に渡す新しいアクション
isPendingアクションが処理中かどうかを示す boolean

基本的な使い方

import { useActionState } from "react";
 
// フォームのアクション関数
async function submitAction(prevState, formData) {
  const name = formData.get("name");
  const email = formData.get("email");
 
  // バリデーション
  if (!name || !email) {
    return { error: "全ての項目を入力してください", success: false };
  }
 
  try {
    await saveToDatabase({ name, email });
    return { message: "送信が完了しました!", success: true };
  } catch (e) {
    return { error: "送信に失敗しました。もう一度お試しください。", success: false };
  }
}
 
function ContactForm() {
  const [state, formAction, isPending] = useActionState(submitAction, {
    message: "",
    error: "",
    success: false,
  });
 
  return (
    <form action={formAction}>
      {state.success && (
        <p style={{ color: "green" }}>{state.message}</p>
      )}
      {state.error && (
        <p style={{ color: "red" }}>{state.error}</p>
      )}
 
      <div>
        <label htmlFor="name">お名前</label>
        <input id="name" name="name" type="text" required />
      </div>
 
      <div>
        <label htmlFor="email">メールアドレス</label>
        <input id="email" name="email" type="email" required />
      </div>
 
      <button type="submit" disabled={isPending}>
        {isPending ? "送信中..." : "送信する"}
      </button>
    </form>
  );
}

アクション関数の仕組み

useActionState に渡すアクション関数は、通常の <form action> に渡す関数と少し異なります。

// 通常のフォームアクション
async function normalAction(formData) {
  const name = formData.get("name");
}
 
// useActionState 用のアクション(第1引数に前の state が追加される)
async function actionWithState(prevState, formData) {
  const name = formData.get("name");
  // prevState で前回の結果を参照できる
  console.log("前回の状態:", prevState);
}

この「前の state を受け取る」という設計により、送信回数をカウントしたり、前回のエラーをリセットしたりできます。

送信回数をカウントする例

async function incrementAction(prevState) {
  return prevState + 1; // 前の値に1を足すだけ
}
 
function Counter() {
  const [count, formAction] = useActionState(incrementAction, 0);
 
  return (
    <form action={formAction}>
      <p>送信回数: {count}</p>
      <button type="submit">+1</button>
    </form>
  );
}

useActionState vs useState + useEffect

従来の方法と比較してみます。

// ❌ 従来:useState + useEffect の組み合わせ(煩雑)
function OldForm() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [result, setResult] = useState(null);
  const [error, setError] = useState(null);
 
  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    try {
      const res = await submit(new FormData(e.target));
      setResult(res);
    } catch (err) {
      setError(err.message);
    } finally {
      setIsSubmitting(false);
    }
  };
 
  return <form onSubmit={handleSubmit}>...</form>;
}
 
// ✅ useActionState:シンプルに同じことができる
function NewForm() {
  const [state, formAction, isPending] = useActionState(submitAction, null);
 
  return <form action={formAction}>...</form>;
}

サーバーアクションとの組み合わせ(Next.js / React 19+)

Next.js の Server Actions と組み合わせると、サーバーサイドの処理もシームレスに統合できます。

// app/actions.ts(サーバーアクション)
"use server";
 
export async function saveContact(prevState, formData) {
  const name = formData.get("name");
 
  // サーバー側で DB に保存
  await db.contacts.create({ name });
  return { message: "保存しました!" };
}
// app/ContactForm.tsx(クライアントコンポーネント)
"use client";
 
import { useActionState } from "react";
import { saveContact } from "./actions";
 
export function ContactForm() {
  const [state, formAction, isPending] = useActionState(saveContact, null);
 
  return (
    <form action={formAction}>
      <input name="name" />
      <button disabled={isPending}>保存</button>
      {state?.message && <p>{state.message}</p>}
    </form>
  );
}

まとめ

  • useActionState はフォームアクションの結果を state として管理するフック
  • [state, formAction, isPending] の3つを返す
  • アクション関数の第1引数に「前の state」が渡される
  • isPending でローディング状態の表示が簡単にできる
  • React 19 の Server Actions と組み合わせることで、Progressive Enhancement が実現できる

関連記事