12.1 Action

React 19では、Transition内で実行される非同期関数をActionとして扱い、pending、エラー、楽観的更新などを構成しやすくなった。

const [isPending, startTransition] = useTransition();

function handleSubmit() {
  startTransition(async () => {
    const result = await updateProfile(form);

    if (!result.ok) {
      setError(result.error);
      return;
    }

    setMessage("保存しました");
  });
}

12.2 <form action>

async function saveUser(formData: FormData) {
  const name = String(formData.get("name") ?? "");
  await api.saveUser({ name });
}

function UserForm() {
  return (
    <form action={saveUser}>
      <input name="name" />
      <SubmitButton />
    </form>
  );
}

関数Actionが成功すると、非制御フォームの入力が自動的にリセットされる。

12.3 useActionState

type FormState = {
  error: string | null;
  success: boolean;
};

async function save(
  previousState: FormState,
  formData: FormData
): Promise<FormState> {
  const name = String(formData.get("name") ?? "").trim();

  if (!name) {
    return {
      error: "名前を入力してください。",
      success: false,
    };
  }

  await api.save({ name });

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

function Form() {
  const [state, action, isPending] = useActionState(save, {
    error: null,
    success: false,
  });

  return (
    <form action={action}>
      <input name="name" />
      <button disabled={isPending}>
        {isPending ? "保存中..." : "保存"}
      </button>
      {state.error && <p role="alert">{state.error}</p>}
    </form>
  );
}

12.4 useFormStatus

フォーム配下の送信状態を読み取る。

function SubmitButton() {
  const { pending, data, method, action } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? "送信中..." : "送信"}
    </button>
  );
}

useFormStatusを呼ぶコンポーネントは、対象のformの子孫でなければならない。

12.5 useOptimistic

サーバー応答を待たず、成功したと仮定したUIを表示する。

const [optimisticMessages, addOptimisticMessage] = useOptimistic(
  messages,
  (current, text: string) => [
    ...current,
    {
      id: `optimistic-${Date.now()}`,
      text,
      sending: true,
    },
  ]
);

async function sendMessage(formData: FormData) {
  const text = String(formData.get("message") ?? "");
  addOptimisticMessage(text);
  await api.sendMessage(text);
}

失敗時にどう戻すか、重複送信をどう扱うか、サーバー側の最終結果とどう整合させるかを設計する。

12.6 フォーム設計原則

  • HTMLの標準機能を活用する
  • nameを付ける
  • labelを関連付ける
  • サーバー側でも検証する
  • pending中の多重送信を防ぐ
  • フィールドエラーと全体エラーを分ける
  • 楽観的更新は取り消し可能性を考える
  • Actionを認可境界として信頼しない