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}> の部分——onSubmit で e.preventDefault() しなくていい。React 19 から form の action 属性に非同期関数を渡せるようになった。
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 の出番だ。いつ書いても同じになるコードは、フレームワークが肩代わりしてくれた方がいい。