React 19の新機能を活用したシンプルで直感的なフォーム送信
はじめに
少しばかり遅いですがReact 19 の新機能として追加された useActionState と、フォームの action 属性の新しい使い方を試してみました。フォーム送信の簡略化と、非同期処理の状態管理がどのように変わるのかを検証し、これまでのフォーム処理と比べてどのようにコードが簡潔になるのか、また、非同期処理をシンプルに管理するための useActionState の具体的な使い方を紹介します。
さらに、従来の useReactForm との比較を通じて、それぞれの利点と活用シーンについても触れています。フォーム送信の実装や状態管理に悩んでいる方や、React 19 の新しい可能性を試してみたい方に向けた記事になっています
今回紹介するコードはGitHubでも公開しています。
実際に試してみたい方は、ぜひこちらもご確認ください
action 属性とは?
<form> の action 属性
React 19 では、フォームの action 属性に直接関数を渡せるようになりました。この関数には FormData オブジェクトが引数として渡され、簡単にフォームのデータを扱うことができます。
React 18以前とReact 19の比較
- React 18以前のフォーム送信方法
React 18以前では、フォームデータを送信する際、onSubmitとuseStateを使用してデータを管理していました。
export default function Form() {
const [formData, setFormData] = useState({ name: "", email: "" });
function handleChange(e) {
setFormData({ ...formData, [e.target.name]: e.target.value });
}
function handleSubmit(e) {
e.preventDefault();
console.log("Submitted:", formData);
}
return (
<form onSubmit={handleSubmit}>
<input name="name" onChange={handleChange} />
<input name="email" onChange={handleChange} />
<button type="submit">Submit</button>
</form>
);
}
React 19でのフォーム送信方法
React 19では、action属性を活用することで、フォームデータの送信がよりシンプルに記述できます。
export default function Form() {
async function handleSubmit(formData) {
console.log("Submitted:", Object.fromEntries(formData.entries()));
}
return (
<form action={handleSubmit}>
<input name="name" />
<input name="email" />
<button type="submit">Submit</button>
</form>
);
}
action属性により、従来の状態管理や冗長なコードから解放され、シンプルで効率的なフォーム処理が実現できます。
useActionStateの説明の前にuseTransitionについて少し触れる
useActionStateを効果的に理解するには、React 18で導入されたuseTransitionの基本的な概念を知っておくと役立ちます。
useTransitionの役割
Reactの非同期UI更新を管理するためのフック。
ユーザー体験を向上させるため、画面全体の状態がブロックされることを防ぎながら、バックグラウンドで非同期処理を進めます。
useTransitionの使い方
以下は、useTransitionの基本的な使い方を示した例です。
import { useState, useTransition } from "react";
export default function Example() {
const [count, setCount] = useState(0);
const [isPending, startTransition] = useTransition();
function handleClick() {
startTransition(() => {
setCount((prev) => prev + 1);
});
}
return (
<div>
<button onClick={handleClick} disabled={isPending}>
{isPending ? "Loading..." : "Increment"}
</button>
<p>Count: {count}</p>
</div>
);
}
このコードでは、startTransitionを利用して状態更新をバックグラウンドで実行し、メインのUIをスムーズに保っています。
useStateとの違い
useStateは状態を即時的に更新するのに対し、useTransitionは非同期処理中の状態管理に特化しています。以下の表でその違いを比較します。
特徴 | useState | useTransition |
---|---|---|
目的 | 状態の即時更新と再レンダリング | 非同期処理中の状態管理 |
再レンダリングのタイミング | 状態更新後、即時 | 再レンダリングの優先順位を調整可能 |
用途 | シンプルな状態管理 | 非同期処理中のUIスムーズさを保つ |
UIへの影響 | 状態更新が頻繁に発生する場合、UIがフリーズする可能性 | メインUIをブロックせずスムーズに処理する |
さてuseActionStateとは
useActionStateは、useTransitionをさらに発展させたものとして、React 19で登場しました。特にフォーム送信に特化しており、以下の点でuseTransitionを補完しています。
- フォーム送信の状態管理
isPendingを用いて、送信中かどうかを簡単に判断。 - 成功・失敗の結果の一元化:
フォーム送信結果(成功・失敗)をstateとして一括管理。 - UI更新の簡素化
非同期処理とUI更新を1つのフックで実現。
基本的な使い方(シンプルなFormState)
"use client";
import { useActionState } from "react";
type FormState = {
success: boolean;
};
export default function SimpleForm() {
async function handleSubmit(
state: FormState | null,
formData: FormData
): Promise<FormState> {
const data = Object.fromEntries(formData.entries());
if (!data.name || !data.email) {
return { success: false }; // バリデーション失敗時
}
return new Promise((resolve) => {
setTimeout(() => resolve({ success: true }), 2000); // バリデーション成功時
});
}
const [state, formAction, isPending] = useActionState<FormState, FormData>(
handleSubmit,
{ success: false }
);
return (
<form action={formAction}>
<input name="name" placeholder="Name" />
<input name="email" placeholder="Email" />
<button type="submit" disabled={isPending}>
{isPending ? "Submitting..." : "Submit"}
</button>
{state.success && <p>Form submitted successfully!</p>}
</form>
);
}
useActionStateの特徴
FormDataが自動的に渡される
通常のReactフォームでは、onSubmitイベントから手動でフォームデータを取得する必要があります。例えば、以下のようにevent.preventDefault()を使用し、new FormData()を明示的に作成していました。
function handleSubmit(event: React.FormEvent) {
event.preventDefault();
const formData = new FormData(event.target as HTMLFormElement);
console.log(Object.fromEntries(formData.entries()));
}
しかし、useActionStateを利用すると、フォームのaction属性に指定した関数に、自動的にFormDataが渡されます。これにより、フォームデータの取得が不要になり、記述が大幅に簡略化されます。
フォームの状態を一括管理
useActionStateでは、フォームの送信状態や結果をstateとして一括で管理できます。これにより、次のようなメリットがあります。
成功状態の簡単な追跡: フォーム送信が成功したかどうかをstate.successで判別可能。
例えば、このコードでは、成功時と失敗時の結果を管理しています。
async function handleSubmit(
_state: { success: boolean } | null,
formData: FormData
): Promise<{ success: boolean }> {
const data = Object.fromEntries(formData.entries());
if (!data.name || !data.email) {
return { success: false }; // バリデーション失敗時
}
return new Promise((resolve) => {
setTimeout(() => resolve({ success: true }), 2000); // バリデーション成功時
});
}
UI更新の一貫性
isPendingを利用して、送信中かどうかを判定できます。例えば、以下のように処理中のボタンを「Submitting...」に変えたり、無効化するのも簡単です。
<button type="submit" disabled={isPending}>
{isPending ? "Submitting..." : "Submit"}
</button>
{state?.success && (
<p className="text-green-500">Form submitted successfully!</p>
)}
バリデーション処理もstateで一元管理
通常のバリデーションと比較
従来のReactでは、フォームバリデーションを実装する際に、エラーメッセージを個別にuseStateで管理する必要がありました。以下のようなコードになりがちです。
const [errors, setErrors] = useState<Record<string, string>>({});
function handleValidation(formData: FormData) {
const data = Object.fromEntries(formData.entries());
const newErrors: Record<string, string> = {};
if (!data.name) newErrors.name = "Name is required.";
if (!data.email) newErrors.email = "Email is required.";
setErrors(newErrors);
}
これに対して、useActionStateを使うと、stateの中にerrorsを含めることで、エラー状態を簡単に管理できます。
"use client";
import { useActionState } from "react";
type FormState = {
success: boolean;
errors?: Record<string, string>;
};
export default function SimpleForm() {
async function handleSubmit(
_state: FormState | null,
formData: FormData
): Promise<FormState> {
const errors = validateFormData(formData);
if (Object.keys(errors).length > 0) {
return { success: false, errors };
}
return { success: true };
}
const [state, formAction, isPending] = useActionState<FormState, FormData>(
handleSubmit,
{ success: false }
);
const errors = state?.errors || {};
return (
<form action={formAction} className="space-y-4">
<div>
<label>
Name
<input
name="name"
className={errors.name ? "border-red-500" : ""}
/>
</label>
{errors.name && <p className="text-red-500">{errors.name}</p>}
</div>
<button type="submit" disabled={isPending}>
{isPending ? "Submitting..." : "Submit"}
</button>
{state.success && <p className="text-green-500">Success!</p>}
</form>
);
}
useActionState は useReactForm の代わりになるか?
この記事を読んでいる方の中には、日頃から useReactForm を使ってフォームを実装されている方も多いのではないでしょうか。かくいう私も、これまで useReactForm を使って多くのフォームを作ってきました。しかしながら、テストが辛い...
触ってみた感じの感想としてはケースバイケースだと思います(そりゃそうだ)
useActionState は、小規模でシンプルなフォーム管理には非常に有効ですが、複雑なフォームや高度なバリデーションが求められる場面では、useReactForm の柔軟性が必要になるでしょう。
useActionState が有効な場面
- 小規模・単純なフォーム
- 軽量なプロジェクト
- 複雑な依存関係が不要な場面で、よりシンプルな選択肢を求める場合。
useReactForm が有効な場面
- フィールド数が多い、動的にフィールドが増減するような複雑なフォーム
- 高度なバリデーション
テストのしやすさの違い
useActionState の特徴として、テストの簡便さが挙げられます。状態を管理する state が返されるため、以下のように状態のモックだけでテストが可能です。
test("フォーム送信時にエラーメッセージが表示される", () => {
const mockState = { success: false, errors: { name: "Name is required." } };
const { getByText } = render(
<SimpleForm useActionState={() => [mockState, jest.fn(), false]} />
);
expect(getByText("Name is required.")).toBeInTheDocument();
});
一方、useReactForm では個々のフィールドの状態を細かくモックする必要があるため、テストの負担が増す場合があります。
どちらも適材適所で選択し、プロジェクトの規模や要件に応じて使い分けるのがベストです。
シンプルな非同期フォームを構築したい場合、useActionState を試してみる価値があると思います。
まとめ
React 19で登場したuseActionStateとフォームのaction属性は、従来の状態管理の煩雑さから開放され、シンプルで直感的なフォーム処理を提供します。特に以下の点で、その魅力を実感しました。
フォーム送信処理の簡略化action属性により、手動でFormDataを取得する必要がなくなり、送信処理の記述が大幅に簡潔化されました。
非同期処理とUI更新の一貫性useActionStateを使うことで、送信中の状態(isPending)や成功状態(state.success)を簡単に管理できるようになり、UIの一貫性が向上しました。
バリデーション処理の簡便化バリデーションエラーをstate内に含めることで、エラー表示の管理が統一され、記述量が減りました。
課題と使い分け
useActionStateは、シンプルなフォームには最適ですが、リアルタイムバリデーションや動的なフィールドが必要な複雑なフォームではuseReactFormの方が適しています。それぞれの特徴を理解し、プロジェクトの要件に応じて使い分けることが重要です。
これからの展望
useActionStateは軽量なフォーム構築には非常に有効で、特に非同期処理を伴うフォームには適しています。一方で、フォームの柔軟性や高度なバリデーションが求められる場面では、useReactFormなどの専用ライブラリがまだ有利と言えるでしょう。
今後は、この2つを組み合わせたハイブリッドなアプローチや、さらなる拡張性を模索しながら、プロジェクトに最適なソリューションを探っていきたいと思います。
Discussion