ボイラープレートを脱却:React 19のActionsとuseActionStateをマスターする

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

従来のReactフォームの煩雑な現実

Reactでのフォーム処理は、かつては新しい機能を実装するたびに支払わなければならない税金のようなものでした。シンプルな「プロフィールの更新」フォームを作りたいだけでも、単にHTMLを書くだけでは済みません。複雑なステートマシンを構築する必要がありました。すべての入力フィールドにuseStateを用意し、loadingの切り替え、error文字列、 school そしてブラウザのネイティブな挙動を手動で制御するhandleSubmit関数が必要でした。

あるダッシュボードの監査をした際、たった一つのログインフォームに、return文に到達するまで45行ものボイラープレートが必要だったことを覚えています。正気を保つために、私たちはFormikやReact Hook Formのような重量級のライブラリに頼ることがよくありました。これらのツールは強力ですが、バンドルサイズの増加や、新入社員が学習しなければならないコストが伴います。本当の問題はツールの不足ではなく、Reactが非同期のデータ変更(mutation)のライフサイクルをネイティブに理解していなかったことでした。

なぜフォームロジックを過剰に作り込んでしまったのか

この摩擦は、根本的なミスマッチに起因します。ReactはステートをUIに同期するように設計されました。しかし、フォームの送信は単なる状態の変化ではなく、「トランザクション」です。React 19以前のフレームワークには、複数のレンダリングにまたがる「Action(アクション)」という組み込みの概念がありませんでした。

Reactが非同期操作の開始や終了を追跡できなかったため、私たちは自分たちでそれを行う必要がありました。その結果、多くのコードベースを悩ませる、恐ろしい「ステート・スープ(状態のスパゲッティ化)」パターンが生まれました。

const [data, setData] = useState(null);
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState(null);

async function handleSubmit(e) {
  e.preventDefault();
  setIsPending(true);
  try {
    const result = await updateProfile(new FormData(e.target));
    setData(result);
  } catch (err) {
    setError(err);
  } finally {
    setIsPending(false);
  }
}

このパターンは、ウェブ上のいたるところで何百万回と繰り返されています。これは脆弱でバグが発生しやすく、finallyブロックでローディング状態をリセットし忘れるのは典型的なミスです. 率直に言って、大規模な環境でこれを維持するのは非常に疲れる作業です。

転換点:ネイティブ機能 vs サードパーティライブラリ

React 19はこの状況を一変させます。山のような手動コードを書くか、15KBのライブラリをインポートするか、という選択を迫られることはもうありません。より高速で軽量、かつ読みやすい、ネイティブな中間層が手に入ったのです。

React 19のパターンを本番環境に導入したところ、その差は歴然でした。私のチームはフォーム関連の依存関係をいくつか削除することに成功し、gzip圧縮後のバンドルサイズを約14.2KB削減できました。アプローチの違いは以下の通りです:

  • 手動のステート管理: 完全に制御できますが、時間の30%を同じtry/catch/finallyブロックを書くことに費やすことになります。
  • 外部ライブラリ: 50個の入力があるような複雑なスキーマには最適ですが、「ベンダーロックイン」が発生し、単純なタスクには不必要な重荷となります。
  • React 19 Actions: 依存関係はゼロです。Concurrent Modeと統合されており、保留(pending)状態を自動的に処理します。

React 19 Actionsの実装:本番環境で使えるパターン

React 19では「Actions」が導入されました。これはトランザクションベースのロジックを使用してデータを管理する関数です。ここで重要な役割を果たすのがuseActionStateです(以前の実験的ビルドではuseFormStateと呼ばれていたものです)。

このフックは、アクション関数と初期状態を受け取ります。そして、現在の状態、フォームで使用するためのラップされたアクション、そしてシンプルなisPendingブール値を返します。

モダンなフォームパターン

標準的なフォームを、クリーンなReact 19の実装にリファクタリングしてみましょう。まず、更新のためのロジックを定義します:

// actions.js
export async function updateUsername(prevState, formData) {
  const newName = formData.get("username");
  
  // 1.5秒のAPI遅延をシミュレート
  await new Promise(res => setTimeout(res, 1500));

  if (newName === "admin") {
    return { error: "その名前は予約されています", success: false };
  }

  return { error: null, success: true, name: newName };
}

次に、これをコンポーネントに組み込みます。onSubmite.preventDefault()が不要になったことに注目してください:

import { useActionState } from "react";
import { updateUsername } from "./actions";

function ProfileForm() {
  const [state, formAction, isPending] = useActionState(updateUsername, {
    error: null,
    success: false,
  });

  return (
    <form action={formAction}>
      <input type="text" name="username" disabled={isPending} />
      <button type="submit" disabled={isPending}>
        {isPending ? "保存中..." : "名前を更新"}
      </button>
      
      {state.error && <p className="error">{state.error}</p>}
      {state.success && <p className="success">{state.name} に変更されました!</p>}
    </form>
  );
}

useFormStatusで深い階層のUI更新を解決する

レイアウトの3階層奥深くに配置された送信ボタンを無効化しなければならなかった時のことを思い出してください。isPendingをpropsでバケツリレーするのは悪夢です。React 19はこれを解決するためにuseFormStatusを提供しています。これは、すべての<form>を自動的にラップする、特化型のContextプロバイダーのように機能します。

import { useFormStatus } from "react-dom";

function SubmitButton() {
  const { pending } = useFormStatus();
  
  return (
    <button type="submit" disabled={pending}>
      {pending ? "処理中..." : "変更を保存"}
    </button>
  );
}

SubmitButtonコンポーネントは、親フォームが送信中であるかどうかを、間にいくつのコンポーネントが挟まっていても検知できるようになります。

いつこれを使うべきか?

これらの機能を本番のコードベースで半年間運用してきた経験から、要件の90%にはネイティブのActionsを使用することをお勧めします。formData APIは驚くほど有能です。追加の設定なしで、ファイルのアップロード、チェックボックス、マルチセレクトなどを処理できます。

ただし、以下の3つのポイントを念頭に置いておいてください:

  1. バリデーション: 基本的なチェックには、requiredpatternのようなHTML5属性を使いましょう。複雑で複数ページにわたるスキーマの場合は、ActionsをZodのようなライブラリと組み合わせるのがベストです。
  2. 楽観的UI: ユーザーが「送信」をクリックした瞬間にUIを更新したい場合は、useActionStateuseOptimisticフックと組み合わせてください。
  3. プログレッシブ・エンハンスメント: Actionsは、JavaScriptのバンドルがダウンロードされる前でも動作します。これは、低速な3G接続や古いデバイスを使用しているユーザーにとって大きなメリットです。

最後に

Actionsへの移行は、キーストロークを細かく管理することから脱却し、ユーザーの意図に集中することへの転換を意味します。useActionStateuseFormStatusを採用することで、コード量は減り、バンドルサイズは小さくなり、より堅牢なアプリを構築できます。React 19で新しいプロジェクトを始めるなら、まずはネイティブな方法を試してみてください。かつてほどサードパーティのフォームライブラリが必要ではないことに気づくはずです。

Share: