⚠️ この記事について
この記事はAI(Claude)を活用して作成しています。
以下の点にご注意ください:
- 情報の鮮度: 記事作成時点(2026年6月)の情報です。ライブラリのバージョンアップ等により内容が古くなる場合があります
- 動作確認: コードサンプルは必ず実際の環境でテスト・検証してください
- 出典の確認: 重要な実装判断は本文中の参考リンクや公式ドキュメントで必ず一次確認をお願いします
- 誤りの可能性: AI生成コンテンツには誤りが含まれる場合があります。お気づきの点はコメントでご指摘ください
はじめに
React でフォーム送信や非同期処理を実装するとき、こんなコードを書いたことはありませんか?
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError(null);
try {
const result = await submitForm(formData);
setData(result);
} catch (err) {
setError('エラーが発生しました');
} finally {
setIsLoading(false);
}
};
ローディング・エラー・データのために useState を3つ並べ、try/catch/finally で状態をひとつひとつ管理する…。これはほぼすべてのフォームで繰り返されるボイラープレートです。
React 19 では useActionState と useOptimistic という2つの新しいHooksが正式に導入され、このような定型コードを大幅に削減できるようになりました。本記事ではその使い方を実例とともに解説します。
環境 / 前提条件
| 項目 | バージョン |
|---|---|
| React | 19.2.4 以上 ※ |
| Node.js | 18.x 以上 |
| TypeScript | 5.x(任意) |
※ React 19系にはServer Componentsに関する複数のセキュリティ脆弱性が報告されており、バージョン 19.0.4 / 19.1.5 / 19.2.4 以降へのアップグレードが強く推奨されています1。
インストール:
npm install react@latest react-dom@latest
従来のフォーム処理の課題
React 18以前の典型的なフォーム実装では、1つのフォーム送信に対して最低でも以下の状態管理が必要でした2:
-
isLoading(通信中かどうか) -
error(エラーメッセージ) -
data(レスポンスデータ) - 楽観的UIが必要な場合はさらに「送信前のスナップショット」と「手動ロールバック処理」
これが30〜50行のボイラープレートとなり、コンポーネントを肥大化させる原因になっていました。
useActionState で状態管理をまとめる
基本構文
useActionState は非同期アクション(Action)の「ペンディング状態・結果・エラー」を一括管理するHooksです3。
import { useActionState } from 'react';
const [state, dispatchAction, isPending] = useActionState(actionFn, initialState);
| 返り値 | 説明 |
|---|---|
state |
アクション実行後の最新状態(初回は initialState) |
dispatchAction |
アクションを呼び出す関数 |
isPending |
アクション実行中は true
|
実例:ログインフォーム
import { useActionState } from 'react';
type FormState = {
success: boolean | null;
error: string | null;
};
async function loginAction(
prevState: FormState,
formData: FormData
): Promise<FormState> {
const email = formData.get('email') as string;
const password = formData.get('password') as string;
// バリデーション
if (!email || !password) {
return { success: false, error: 'メールアドレスとパスワードを入力してください' };
}
try {
await apiLogin({ email, password }); // 実際のAPI呼び出し
return { success: true, error: null };
} catch {
return { success: false, error: 'ログインに失敗しました' };
}
}
export function LoginForm() {
const [state, submitAction, isPending] = useActionState(loginAction, {
success: null,
error: null,
});
return (
<form action={submitAction}>
<input name="email" type="email" placeholder="メールアドレス" />
<input name="password" type="password" placeholder="パスワード" />
<button type="submit" disabled={isPending}>
{isPending ? 'ログイン中...' : 'ログイン'}
</button>
{state.error && <p style={{ color: 'red' }}>{state.error}</p>}
{state.success && <p style={{ color: 'green' }}>ログインしました!</p>}
</form>
);
}
ポイントは以下の3点です:
-
useStateが不要:ローディング・エラー・結果をすべてuseActionStateが管理 -
formのaction属性に関数を渡せる:React 19 では<form action={fn}>の形式をサポート3 - アクション関数は純粋に書ける:副作用のロジックをコンポーネント外に切り出せる
useOptimistic で即時UIフィードバックを実現する
楽観的UI(Optimistic UI)とは
ネットワーク通信の完了を待たずに、UIを先に更新して「いかにも処理が終わった」ように見せる手法です。サーバーからエラーが返ってきたときは自動的に元の状態へ戻します。
基本構文
import { useOptimistic } from 'react';
const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);
| 引数/返り値 | 説明 |
|---|---|
state |
実際のデータ(サーバーから取得した正の状態) |
updateFn |
楽観的な更新を定義する関数 (currentState, optimisticValue) => newState
|
optimisticState |
楽観的に更新されたUI用の状態 |
addOptimistic |
楽観的な更新を適用する関数 |
実例:Todoリスト
import { useActionState, useOptimistic } from 'react';
type Todo = { id: string; text: string; sending?: boolean };
async function addTodoAction(
prevTodos: Todo[],
formData: FormData
): Promise<Todo[]> {
const text = formData.get('text') as string;
const newTodo = await createTodo({ text }); // API呼び出し
return [...prevTodos, newTodo];
}
export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
const [todos, submitAction, isPending] = useActionState(addTodoAction, initialTodos);
// 楽観的UIの設定
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(currentTodos: Todo[], newText: string) => [
...currentTodos,
{ id: `temp-${Date.now()}`, text: newText, sending: true },
]
);
const handleAction = async (formData: FormData) => {
const text = formData.get('text') as string;
// ← awaitより前に呼び出すことが重要[^4]
addOptimisticTodo(text);
await submitAction(formData);
};
return (
<div>
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id} style={{ opacity: todo.sending ? 0.5 : 1 }}>
{todo.text}
{todo.sending && ' (送信中...)'}
</li>
))}
</ul>
<form action={handleAction}>
<input name="text" placeholder="新しいTodo" />
<button type="submit" disabled={isPending}>追加</button>
</form>
</div>
);
}
useOptimistic の仕組み
useOptimistic はReactのトランジションシステムに連動しています2。アクションが完了すると:
-
成功した場合:
state(サーバーからの正の状態)に自動的に置き換わる -
失敗した場合:
stateは変化せず、楽観的な更新は自動的に消える
手動でスナップショットを取ったり、catch でロールバック処理を書く必要がありません。
useActionState と useOptimistic の使い分け
| 用途 | 推奨Hook |
|---|---|
| フォーム送信のローディング・エラー管理 | useActionState |
| 送信中に即座にUIを更新したい | useOptimistic |
| 両方が必要(送信 + 楽観的UI) | 組み合わせて使う |
両者は補完関係にあります。useActionState でアクションのライフサイクルを管理しつつ、useOptimistic で即時フィードバックを提供するのが典型的なパターンです2。
React Queryなどのライブラリとの使い分け
useActionState と useOptimistic はコンポーネント内のローカルなアクション状態管理に特化しています。グローバルなサーバーキャッシュ・バックグラウンドリフェッチ・複数コンポーネント間のキャッシュ同期が必要な場合は、引き続き React Query(TanStack Query)や SWR が有効です2。
まとめ
-
useActionStateはフォーム送信など非同期アクションの「ペンディング状態・結果・エラー」を1つのHooksで管理し、複数のuseStateとtry/catch/finallyボイラープレートを不要にする -
useOptimisticは楽観的UI更新を宣言的に記述でき、失敗時の自動ロールバックが組み込まれている - 2つを組み合わせることで、以前30〜50行必要だったフォームロジックを約12行程度に圧縮できる2
- React 19系を使う場合はセキュリティパッチ済みの
19.0.4/19.1.5/19.2.4以上を使用すること1
参考情報
-
Denial of Service and Source Code Exposure in React Server Components - react.dev - 参照日: 2026-06-28 ↩ ↩2
-
React 19's useOptimistic and useActionState: Replacing 80% of Your State Boilerplate - SitePoint - 参照日: 2026-06-28 ↩ ↩2 ↩3 ↩4 ↩5
-
React v19 – React公式ブログ - react.dev - 参照日: 2026-06-28 ↩ ↩2