🌟

React19で追加された 3 つの hook(useActionState, useFormStatus, useOptimistic)

に公開

React v19 の正式リリースから約半年が経ちました。

新しいバージョンのリリース直後は安定性への懸念もあり、すぐに導入するのは勇気がいるものです。しかし半年が経ち、そろそろ本格的なアップグレードを検討している方も多いのではないでしょうか。
自分もその一人です。。

そこで今回は、React19 で新たに追加された 3 つの hook(useActionStateuseFormStatususeOptimistic)に焦点を当て、従来の実装方法と詳細に比較しながら、これらの新機能を完全にマスターするための解説記事を書きました!

この記事を読むことで、React19 の新機能を活用して、従来よりもはるかに簡潔で、かつ UX のよいフォーム実装ができるようになります。

React19 で追加されたフォーム関連の 3 つの hook

まず、React19 で何が変わったのか、全体像を把握しましょう。

React19 の公式リリースノートはこちらです。

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

このリリースは非常に大規模なアップデートとなっており、主要な変更内容をまとめると以下のような構成になっています。

新機能

アクション関係

  • useActionState hook が新たに追加
  • useFormStatus hook が新たに追加
  • useOptimistic hook が新たに追加
  • use API が新たに追加

静的サイト生成関係

  • prerender API が新たに追加
  • prerenderToNodeStream API が新たに追加

React Server Component 関係

  • RSC が使えるようになった
  • Server Actions が使えるようになった

改善点

  • forwardRef が不要となり、props のように ref を扱えるようになった(将来的には forwardRef は deprecate)
  • ハイドレーションエラーのメッセージがわかりやすくなった
  • <Context.Provider>の記述が不要となり、<Context>で OK になった(将来的には<Context.Provider>は deprecate)
  • ref がアンマウント時にクリーンアップできるようになった
  • useDeferredValue に初期値を指定できるようになった
  • meta タグなどの metadata をコンポーネントで扱えるようになった

この中でも特に React19 での最大の目玉機能と言える、フォーム処理に関連する 3 つの hook について詳しく解説していきます。

forwardRef が不要になるなど他にも開発体験を大きく向上させる重要なアップデートが多数ありますが、今回は最も実用的で、日常的な開発で頻繁に使用することになる部分にフォーカスした内容としています。

useActionState:フォーム状態管理が 1 行で完結する革命的 hook

https://ja.react.dev/reference/react/useActionState

useActionState とは何か

useActionState は、フォームアクションの結果に基づいて state を更新するためのフックです。

これまでフォーム処理で必要だった複雑な状態管理(送信中フラグ、エラー状態、成功状態など)を、たった 1 行の hook で管理できるようになる、まさに革命的な機能と言えるでしょう。

以下のシンプルなお問い合わせフォームを例に、従来の実装方法と新しい useActionState を使った実装方法を詳しく比較してみます。

従来の実装では複数の useState が必要だった

従来の React でフォームを実装する場合、フォームデータ、送信状態、エラー状態、成功状態など、それぞれに対して個別の useState を定義し、さらに複雑なイベントハンドラーを記述する必要がありました。

以下は、純粋な React を使った従来の実装例です。

export default function Traditional() {
  const [formData, setFormData] = useState({ name: "", email: "" });
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setError(null);
    setSuccess(false);

    // バリデーション
    if (!formData.name || !formData.email) {
      setError("全ての項目を入力してください");
      return;
    }

    setIsSubmitting(true);
    try {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      setSuccess(true);
      setFormData({ name: "", email: "" });
    } catch {
      setError("送信に失敗しました");
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* 複数の状態管理とイベントハンドラーが必要 */}
    </form>
  );
}

実際の開発現場では、フォーム処理に react-hook-form のようなライブラリを使用することが一般的です。そこで、react-hook-form を使用した場合の実装も確認しておきましょう。

react-hook-form を使用した場合でも、サーバーへの送信処理やその結果の状態管理については、依然として手動で行う必要があります。

// react-hook-form使用例(簡略版)
export default function ReactHookForm() {
  const [submitState, setSubmitState] = useState({});
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    reset,
  } = useForm();

  const onSubmit = async (data) => {
    try {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      setSubmitState({
        success: true,
        message: `${data.name}さん、ありがとうございました!`,
      });
      reset();
    } catch {
      setSubmitState({ error: "送信に失敗しました。" });
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* フォーム要素とエラー表示 */}
    </form>
  );
}

useActionState を使えば驚くほどシンプルになる

上記のような複雑な状態管理が必要だった従来の実装が、useActionState を使うことで驚くほどシンプルに書けるようになります。

以下が、同じ機能を useActionState で実装した例です。

import { useActionState } from "react";

// useActionStateの本質:アクション関数で全ての処理を宣言的に定義
async function submitAction(prevState, formData) {
  const name = formData.get("name");
  const email = formData.get("email");

  // バリデーション
  if (!name || !email) {
    return { error: "全ての項目を入力してください" };
  }

  if (!email.includes("@")) {
    return { error: "正しいメールアドレスを入力してください" };
  }

  // サーバー送信をシミュレート
  await new Promise((resolve) => setTimeout(resolve, 1000));

  return {
    success: true,
    message: `${name}さん、ありがとうございました!`,
  };
}

export default function New() {
  // たった1行でフォーム状態管理が完了
  const [state, formAction, isPending] = useActionState(submitAction, {});

  return (
    <div>
      {/* action属性にformActionを渡すだけでOK */}
      <form action={formAction} className="space-y-4">
        <div>
          <label className="form-label">お名前:</label>
          <input
            type="text"
            name="name"
            disabled={isPending}
            className="form-input"
            placeholder="田中太郎"
          />
        </div>

        <div>
          <label className="form-label">メールアドレス:</label>
          <input
            type="email"
            name="email"
            disabled={isPending}
            className="form-input"
            placeholder="tanaka@example.com"
          />
        </div>

        {/* isPendingで送信状態を管理 */}
        <button
          type="submit"
          disabled={isPending}
          className="btn-primary flex items-center"
        >
          {isPending && <div className="spinner mr-2"></div>}
          <span className="mr-2">📧</span>
          {isPending ? "送信中..." : "送信"}
        </button>
      </form>

      {/* エラー・成功の表示もstateから */}
      {state.error && (
        <div className="alert-error mt-4">
          <span className="mr-2"></span>
          {state.error}
        </div>
      )}

      {state.success && (
        <div className="alert-success mt-4">
          <span className="mr-2"></span>
          {state.message}
        </div>
      )}
    </div>
  );
}

useActionState のコードポイント解説

中でもポイントとなるのは、以下の 3 つの要素です。

1. アクション関数の設計

async function submitAction(prevState, formData) {
  // バリデーション、API呼び出し、エラーハンドリングを一箇所に集約
  return { success: true, message: "送信完了" };
}
  • 第 1 引数prevState:前回の状態(初回は初期値)
  • 第 2 引数formData:フォームから送信された FormData オブジェクト
  • 戻り値:新しい状態オブジェクト(エラー、成功メッセージなど)

2. useActionState の戻り値

const [state, formAction, isPending] = useActionState(submitAction, {});
  • state:現在の状態(エラーメッセージ、成功フラグなど)
  • formAction:form 要素の action 属性に渡す関数
  • isPending:送信中かどうかの boolean 値

3. HTML フォームとの連携

<form action={formAction}>
  <button disabled={isPending}>{isPending ? "送信中..." : "送信"}</button>
</form>
  • action={formAction}でフォーム送信を制御
  • isPendingで送信状態に応じた UI 制御

react-hook-form との組み合わせ時の注意点

useActionState を react-hook-form と組み合わせて使用する場合は、以下の重要な点に注意が必要です。

  • 役割の違いを理解する: react-hook-form はクライアント側のフォーム制御とバリデーション、useActionState はサーバー側の送信処理と状態管理を担当
  • handleSubmit は使用不可: useActionState を使用する場合、HTML の form 要素の action 属性を使用するため、react-hook-form の handleSubmit は使えません
  • 機能の重複を避ける: 両方のライブラリが提供する機能に重複がないか、本当に両方が必要かを事前に検討することが重要

useFormStatus:フォーム内コンポーネントが自動で送信状態を把握

https://ja.react.dev/reference/react-dom/hooks/useFormStatus

useFormStatus とは何か

useFormStatus は、直近のフォーム送信に関するステータス情報(送信中かどうか、送信データ、HTTP メソッドなど)を提供するフックです。

最大の特徴は、フォーム内のコンポーネントが、props を通じて状態を受け取ることなく、自動的に親フォームの送信状態を把握できることです。これにより、コンポーネントの独立性が大幅に向上します。

以下のファイルアップロードフォームを例に、従来の実装方法と新しい useFormStatus を使った実装方法を比較してみます。

従来の実装では props での状態受け渡しが必要だった

従来の React では、フォームの送信状態(isSubmitting、isLoading など)を子コンポーネントに伝えるために、必ず props を通じて状態を渡す必要がありました。

これにより、フォームコンポーネントと子コンポーネント間に密結合が生まれ、コンポーネントの再利用性が低下するという問題がありました。

// 従来の実装例(主要部分のみ)
function SubmitButton({ isUploading }) {
  return (
    <button type="submit" disabled={isUploading}>
      {isUploading ? "アップロード中..." : "ファイルをアップロード"}
    </button>
  );
}

export default function Traditional() {
  const [isUploading, setIsUploading] = useState(false);
  const [error, setError] = useState(null);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    const file = formData.get("file");

    setIsUploading(true);
    try {
      await new Promise((resolve) => setTimeout(resolve, 3000));
    } catch {
      setError("アップロードに失敗しました");
    } finally {
      setIsUploading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="file" name="file" />
      <SubmitButton isUploading={isUploading} />
    </form>
  );
}

useFormStatus を使えば props 不要で状態を取得できる

useFormStatus を使うことで、フォーム内のコンポーネントが親から props を受け取ることなく、自動的にフォームの送信状態を把握できるようになります。

これにより、コンポーネントの独立性が向上し、より再利用しやすいコンポーネントを作成できます。

import { useActionState } from "react";
import { useFormStatus } from "react-dom";

// アクション関数
async function uploadFile(prevState, formData) {
  const file = formData.get("file");
  if (!file || file.size === 0) {
    return { error: "ファイルを選択してください" };
  }

  try {
    await new Promise((resolve) => setTimeout(resolve, 3000));
    return {
      message: `ファイル "${file.name}" のアップロードが完了しました!`,
    };
  } catch {
    return { error: "アップロードに失敗しました" };
  }
}

// useFormStatusを使った送信ボタン(propsなし)
function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button
      type="submit"
      disabled={pending}
      className="btn-primary flex items-center"
    >
      {pending ? "アップロード中..." : "ファイルをアップロード"}
    </button>
  );
}

export default function New() {
  const [state, formAction] = useActionState(uploadFile, {});

  return (
    <form action={formAction} className="space-y-4">
      <input type="file" name="file" />
      <SubmitButton />
    </form>
  );
}

useFormStatus のコードポイント解説

中でもポイントとなるのは、以下の 3 つの要素です。

1. useFormStatus の戻り値

const { pending, data, method, action } = useFormStatus();
  • pending:フォームが送信中かどうかの boolean 値
  • data:送信中の FormData オブジェクト(送信中でなければ null)
  • method:フォームの HTTP メソッド(GET、POST など)
  • action:フォームの action URL

2. コンポーネントの独立性

// propsなしで送信状態を取得
function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? "送信中..." : "送信"}</button>;
}
  • 親コンポーネントから props を受け取る必要がない
  • フォーム内であれば自動的に状態を取得

3. フォーム内での自動検出

<form action={formAction}>
  <input name="file" type="file" />
  <SubmitButton /> {/* 自動的にフォームの状態を検出 */}
</form>
  • <form>要素内に配置するだけで自動的に動作
  • 複数のコンポーネントで同じ状態を共有可能

useFormStatus 使用時の重要な注意事項

useFormStatus を使用する際は、以下の重要な制約と注意点を必ず理解しておく必要があります。

  • フォーム内での使用が絶対必須: useFormStatus は<form>要素内でレンダーされるコンポーネント内でのみ動作します。フォーム外で使用しても機能しません
  • フォーム外では無効: フォーム外で使用するとpending: false, data: nullが常に返され、期待した動作をしません
  • 最近接のフォーム参照: ネストしたフォーム構造では、最も近い親<form>要素の状態のみを取得します
  • react-dom からのインポート: 'react'パッケージではなく'react-dom'パッケージからインポートする必要があります

useOptimistic:楽観的 UI 更新で最高のユーザー体験を実現

https://ja.react.dev/reference/react/useOptimistic

useOptimistic とは何か

useOptimistic は、UI を楽観的に(optimistically)更新するためのフックです。

楽観的更新とは、ユーザーがアクション(メッセージ送信、いいねボタンクリックなど)を実行した瞬間に、サーバーからの応答を待たずに即座に UI を更新する手法です。これにより、ユーザーは待機時間を感じることなく、非常にインタラクティブで快適な体験を得ることができます。

以下のチャットアプリケーションを例に、従来の実装方法と新しい useOptimistic を使った実装方法を比較してみます。

従来の実装では手動での楽観的更新が複雑だった

従来の React で楽観的更新を実装するのは非常に複雑で、多くの開発者にとって敷居の高い機能でした。

楽観的更新を実現するためには、一時的な状態管理、エラー時のロールバック処理、重複送信の防止など、多くの考慮事項があり、以下のような複雑なコードを書く必要がありました。

import { useState, useRef } from "react";

function TraditionalThread({ messages, sendMessage }) {
  const formRef = useRef();
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [optimisticMessages, setOptimisticMessages] = useState([]);

  const handleSubmit = async (e) => {
    e.preventDefault();

    if (isSubmitting) return;

    const formData = new FormData(e.target);
    const messageText = formData.get("message");

    if (!messageText.trim()) return;

    // 手動で楽観的更新
    const tempId = Date.now() + Math.random();
    const optimisticMessage = {
      text: messageText,
      sending: true,
      id: tempId,
    };

    setOptimisticMessages((prev) => [...prev, optimisticMessage]);
    setIsSubmitting(true);
    formRef.current.reset();

    try {
      await sendMessage(formData);
      // 成功時:楽観的メッセージを手動で削除
      setOptimisticMessages((prev) => prev.filter((msg) => msg.id !== tempId));
    } catch {
      // エラー時:楽観的メッセージを手動で削除
      setOptimisticMessages((prev) => prev.filter((msg) => msg.id !== tempId));
      alert("メッセージの送信に失敗しました");
    } finally {
      setIsSubmitting(false);
    }
  };

  // 表示用メッセージ(実際のメッセージ + 楽観的メッセージ)
  const displayMessages = [...messages, ...optimisticMessages];

  return (
    <>
      {displayMessages.map((message, index) => (
        <div
          key={message.id || index}
          className={`chat-message ${
            message.sending ? "chat-message-sending" : "chat-message-sent"
          }`}
        >
          <span className="mr-2">💬</span>
          {message.text}
          {!!message.sending && (
            <small className="ml-2 text-gray-500">(送信中...)</small>
          )}
        </div>
      ))}
      <form
        onSubmit={handleSubmit}
        ref={formRef}
        className="mt-4 flex space-x-2"
      >
        <input
          type="text"
          name="message"
          placeholder="メッセージを入力..."
          className="form-input flex-1"
          disabled={isSubmitting}
          required
        />
        <button type="submit" disabled={isSubmitting} className="btn-primary">
          {isSubmitting ? "送信中..." : "送信"}
        </button>
      </form>
    </>
  );
}

export default function Traditional({ messages, sendMessage }) {
  return (
    <div className="chat-container">
      <TraditionalThread messages={messages} sendMessage={sendMessage} />
    </div>
  );
}

useOptimistic を使えば楽観的更新が簡単に実装できる

上記のような複雑な楽観的更新の実装が、useOptimistic を使うことで驚くほど簡単に記述できるようになります。

useOptimistic は、楽観的更新に必要な複雑な状態管理を内部で自動的に処理してくれるため、開発者は楽観的更新のロジックに集中できます。

import { useOptimistic, useRef } from "react";

function Thread({ messages, sendMessage }) {
  const formRef = useRef();

  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      ...state,
      {
        text: newMessage,
        sending: true,
        id: Date.now() + Math.random(),
      },
    ]
  );

  async function formAction(formData) {
    const messageText = formData.get("message");
    addOptimisticMessage(messageText);
    formRef.current.reset();

    try {
      await sendMessage(formData);
    } catch (error) {
      console.error("メッセージ送信エラー:", error);
    }
  }

  return (
    <>
      {optimisticMessages.map((message, index) => (
        <div
          key={message.id || index}
          className={`chat-message ${
            message.sending ? "chat-message-sending" : "chat-message-sent"
          }`}
        >
          <span className="mr-2">💬</span>
          {message.text}
          {!!message.sending && (
            <small className="ml-2 text-gray-500">(送信中...)</small>
          )}
        </div>
      ))}
      <form action={formAction} ref={formRef} className="mt-4 flex space-x-2">
        <input
          type="text"
          name="message"
          placeholder="メッセージを入力..."
          className="form-input flex-1"
          required
        />
        <button type="submit" className="btn-primary">
          送信
        </button>
      </form>
    </>
  );
}

export default function New({ messages, sendMessage }) {
  return (
    <div className="chat-container">
      <Thread messages={messages} sendMessage={sendMessage} />
    </div>
  );
}

useOptimistic のコードポイント解説

中でもポイントとなるのは、以下の 3 つの要素です。

1. useOptimistic の基本構造

const [optimisticMessages, addOptimisticMessage] = useOptimistic(
  messages, // 実際のデータ
  (state, newMessage) => [...state, { text: newMessage, sending: true }] // 楽観的更新関数
);
  • 第 1 引数:実際のデータ(サーバーから取得した確定データ)
  • 第 2 引数:楽観的更新を行う関数(一時的な状態を生成)
  • 戻り値:[楽観的データ, 楽観的更新関数]

2. 楽観的更新の実行

async function formAction(formData) {
  const messageText = formData.get("message");
  addOptimisticMessage(messageText); // 即座にUIを更新

  try {
    await sendMessage(formData); // サーバーに送信
  } catch (error) {
    // エラー時は自動的に楽観的更新がロールバック
  }
}
  • addOptimisticMessageで即座に UI を更新
  • サーバー処理の成功/失敗に関わらず、自動的に状態が管理される

3. 自動的な状態同期

// 実際のmessagesが更新されると、楽観的更新は自動的にクリアされる
{
  optimisticMessages.map((message, index) => (
    <div className={message.sending ? "sending" : "sent"}>{message.text}</div>
  ));
}
  • サーバーからの応答で実際のデータが更新されると、楽観的更新は自動的に削除
  • 手動でのクリーンアップ処理が不要

まとめ:React19 の 3 つの hook で開発効率が大幅向上

React19 で追加された 3 つの hook(useActionState、useFormStatus、useOptimistic)について、従来の実装方法と比較しながら詳しく解説してきました。

これらの新機能は、単なる便利機能ではなく、フロントエンド開発のパラダイムを大きく変える可能性を秘めた革新的な機能です。

各 hook の特徴と効果

useActionState

  • フォーム状態管理が 1 行で完結
  • 複数の useState が不要になり、コードが大幅に簡素化
  • サーバーアクションとの連携が自然

useFormStatus

  • フォーム内コンポーネントが自動で送信状態を把握
  • props での状態受け渡しが不要
  • コンポーネントの独立性が向上

useOptimistic

  • 楽観的 UI 更新が簡単に実装可能
  • ユーザー体験の大幅な向上
  • 手動での状態管理が不要

実践での活用ポイント

これらの 3 つの hook を組み合わせることで、従来のフォーム実装と比べて以下の大きなメリットが得られます。

  • 開発効率の劇的な向上: 複雑な状態管理のボイラープレートコードが大幅に削減され、開発時間を大幅に短縮できます
  • 保守性の大幅な向上: シンプルで理解しやすい実装により、バグの発生率が低下し、メンテナンスコストが削減されます
  • ユーザー体験の革新的な向上: 楽観的更新により、ユーザーが待機時間を感じない、非常にインタラクティブな UI を実現できます

React19 へのアップグレードを検討している方は、まずこれらの 3 つの hook から始めてみることを強くおすすめします。特にフォームを多用する Web アプリケーションでは、開発効率とユーザー体験の両面で劇的な改善が期待できるそうですね!。

Discussion