React 19 の Actions でフォーム処理がこんなにシンプルになった

React 19 で導入された Actions・useActionState・useFormStatus。ローディング状態・エラーハンドリング・楽観的更新のボイラープレートが大幅に減る。

React でフォームを作るとき、決まって同じコードを書いてきた。

const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault();
  setLoading(true);
  setError(null);
  try {
    await submitData(formData);
  } catch (err) {
    setError('送信に失敗しました');
  } finally {
    setLoading(false);
  }
};

loading・error・try/catch・finally——毎回書く、毎回同じ。これがボイラープレートとして認識されるようになって久しい。

React 19 の Actions は、この定型コードをほぼ消してくれる。

useActionState — 一番使う

useActionState は「非同期アクションの状態(ローディング・エラー・結果)を管理する」フック。

import { useActionState } from 'react';

async function submitAction(prevState: any, formData: FormData) {
  const name = formData.get('name') as string;
  
  const res = await fetch('/api/submit', {
    method: 'POST',
    body: JSON.stringify({ name }),
  });
  
  if (!res.ok) return { error: '送信に失敗しました' };
  return { success: true };
}

function ContactForm() {
  const [state, formAction, isPending] = useActionState(submitAction, null);

  return (
    <form action={formAction}>
      <input name="name" />
      <button type="submit" disabled={isPending}>
        {isPending ? '送信中…' : '送信する'}
      </button>
      {state?.error && <p>{state.error}</p>}
    </form>
  );
}

loading 用の useState が要らなくなった。isPending がそれを担う。try/catch も Action 関数の中に一度書くだけ。

注目するのは <form action={formAction}> の部分——onSubmite.preventDefault() しなくていい。React 19 から formaction 属性に非同期関数を渡せるようになった。

useFormStatus — ネストしたコンポーネントで pending を知る

「送信中はボタンを disabled にしたい」だけなら isPending で事足りる。でも Submit ボタンが別コンポーネントに切り出されていると、props のバケツリレーが必要になる。

useFormStatus はそれを解決する。

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();
  
  return (
    <button type="submit" disabled={pending}>
      {pending ? '送信中…' : '送信する'}
    </button>
  );
}

function ContactForm() {
  const [state, formAction] = useActionState(submitAction, null);

  return (
    <form action={formAction}>
      <input name="name" />
      <SubmitButton /> {/* pending を props で渡さなくていい */}
    </form>
  );
}

useFormStatus は「自分が属する form の状態」を自動で拾う。デザインシステムで再利用可能な SubmitButton コンポーネントを作るときに便利だ。

useOptimistic — 楽観的更新がシンプルに

「いいね!を押したら即座に +1 表示して、裏でサーバーに送る」——楽観的更新(Optimistic Update)と呼ぶやつ。体感のレスポンスを良くするテクニックだが、従来は手で状態を二重管理する必要があった。

useOptimistic はそれを抽象化した。

import { useOptimistic, useActionState } from 'react';

function LikeButton({ initialCount }: { initialCount: number }) {
  const [optimisticCount, addOptimistic] = useOptimistic(
    initialCount,
    (current, delta: number) => current + delta
  );

  const [, likeAction] = useActionState(async () => {
    addOptimistic(1); // 即座に +1
    await fetch('/api/like', { method: 'POST' }); // 裏でサーバーに送る
  }, null);

  return (
    <form action={likeAction}>
      <button type="submit">❤️ {optimisticCount}</button>
    </form>
  );
}

addOptimistic を呼ぶと表示が即座に更新される。サーバーが失敗すれば自動でロールバックされる。

移行は段階的でいい

Actions は既存の useState + onSubmit パターンと共存できる。全部書き換える必要はない。

新規のフォームから少しずつ使い始めるのが現実的だ。React Server Components と組み合わせると Server Actions という形でさらに発展するが、Client Components だけでも変化は十分に感じられる。


「この loading の useState、また書いてる……」と思ったことが一度でもあるなら、useActionState の出番だ。いつ書いても同じになるコードは、フレームワークが肩代わりしてくれた方がいい。

元ネタ: https://react.dev/blog/2024/12/05/react-19