mcpField
S
safe-action-forms

safe-action-forms

github.com/next-safe-action開発11.2k 回インストール8 週間前に更新
リポジトリ

safe-action-forms について

ネイティブフォームからReact Hook Formアダプターまで、next-safe-actionのフォーム連携パターンを解説するスキルです。フォーム状態、検証、楽観的更新に応じたHookの選択を支援します。

主な機能

  1. 01useActionでネイティブフォームを使用
  2. 02useStateActionでフォーム状態を追跡
  3. 03React Hook Formアダプターを接続
  4. 04フォームの楽観的更新に対応
  5. 05FormDataとスキーマ要件を処理

活用シーン

  1. 01シンプルなActionフォームを構築
  2. 02フィールド単位の検証エラーを表示
  3. 03楽観的更新を使うフォームを作成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/next-safe-action/skills --skill safe-action-forms

インストール後、Claude Code で/safe-action-formsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

file-uploads.md
form-actions.md
react-hook-form.md

詳細

next-safe-action フォーム連携

オプション

アプローチ使用する場面
useAction + ネイティブフォームシンプルなフォーム、複雑なバリデーション UI が不要な場合、プログラムによるトリガー
useStateAction + <form action={formAction}>状態の追跡が必要なフォーム、prevResult へのアクセスが必要な場合、コールバックをすべて使用する場合
useHookFormAction(RHF アダプター)フィールド単位のエラーや、変更時/フォーカス解除時のバリデーションが必要な複雑なフォーム
useHookFormOptimisticAction楽観的 UI 更新を行う RHF フォーム
useOptimisticStateAction重複する書き込みをキューに入れて蓄積する必要がある、即時反映の UI(hooks スキルを参照)

クイックスタート — useStateAction フォーム

"use client";

import { useStateAction } from "next-safe-action/hooks";
import { submitContact } from "@/app/actions";

export function ContactForm() {
  const { formAction, result, isPending, hasSucceeded } = useStateAction(submitContact, {
    onSuccess: () => toast.success("Message sent!"),
  });

  return (
    <form action={formAction}>
      <input name="name" required />
      <input name="email" type="email" required />
      <textarea name="message" required />

      {result.validationErrors?.email && (
        <p>{result.validationErrors.email._errors?.[0]}</p>
      )}
      {result.serverError && <p>{result.serverError}</p>}
      {hasSucceeded && <p>Message sent!</p>}

      <button type="submit" disabled={isPending}>
        {isPending ? "Sending..." : "Send"}
      </button>
    </form>
  );
}

注:useStateAction を使うには、サーバーアクションを .action() ではなく .stateAction() で定義する必要があります。また、<form action={formAction}> は生の FormData を渡すため、入力スキーマは FormData を解析する必要があります(例:zod-form-data の zfd.formData({...}))。useAction と useStateAction のどちらを使うべきかについて、詳しい判断表はhooks スキルを参照してください。

クイックスタート — ネイティブフォーム

"use client";

import { useAction } from "next-safe-action/hooks";
import { submitContact } from "@/app/actions";

export function ContactForm() {
  const { execute, result, isPending } = useAction(submitContact);

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        const fd = new FormData(e.currentTarget);
        execute({
          name: fd.get("name") as string,
          email: fd.get("email") as string,
          message: fd.get("message") as string,
        });
      }}
    >
      <input name="name" required />
      <input name="email" type="email" required />
      <textarea name="message" required />

      {result.validationErrors && (
        <p>{result.validationErrors.email?._errors?.[0]}</p>
      )}
      {result.serverError && <p>{result.serverError}</p>}
      {result.data && <p>Message sent!</p>}

      <button type="submit" disabled={isPending}>
        {isPending ? "Sending..." : "Send"}
      </button>
    </form>
  );
}

クイックスタート — React Hook Form アダプター

"use client";

import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { submitContact } from "@/app/actions";

const schema = z.object({
  name: z.string().min(1, "Name is required"),
  email: z.string().email("Invalid email"),
  message: z.string().min(10, "Message must be at least 10 characters"),
});

export function ContactForm() {
  const { form, handleSubmitWithAction, action } = useHookFormAction(
    submitContact,
    zodResolver(schema),
    {
      actionProps: {
        onSuccess: () => toast.success("Message sent!"),
      },
    }
  );

  return (
    <form onSubmit={handleSubmitWithAction}>
      <input {...form.register("name")} />
      {form.formState.errors.name && <p>{form.formState.errors.name.message}</p>}

      <input {...form.register("email")} />
      {form.formState.errors.email && <p>{form.formState.errors.email.message}</p>}

      <textarea {...form.register("message")} />
      {form.formState.errors.message && <p>{form.formState.errors.message.message}</p>}

      {action.result.serverError && <p>{action.result.serverError}</p>}

      <button type="submit" disabled={action.isPending}>
        {action.isPending ? "Sending..." : "Send"}
      </button>
    </form>
  );
}

関連ドキュメント

エントリーポイント

パッケージエントリーポイントエクスポート
@next-safe-action/adapter-react-hook-formデフォルトmapToHookFormErrors、型
@next-safe-action/adapter-react-hook-form/hooksフックuseHookFormAction、useHookFormOptimisticAction、useHookFormActionErrorMapper

コメント