レビュー済み·難易度: 中級·更新: 2026-04-17

React Hook Form の useFieldArray で動的フィールドを追加・削除する

useFieldArray を使い、フォーム内のフィールドを動的に追加・削除・並び替えする実装例。Zod で配列バリデーションも行う。

nextjsformvalidationreact-hook-formzod

対応バージョン

nextjs 15react 19react-hook-form 7zod 3

前提環境

React Hook Form の useForm と zodResolver の基本を理解していること

概要

useFieldArray を使うと、フォーム内に動的なフィールドのリストを管理できる。 メンバー追加フォームや、複数行の入力テーブルなど、件数が可変のフォームに使う。

インストール

npm install react-hook-form zod @hookform/resolvers

実装例(メンバー追加フォーム)

// src/components/MemberForm.tsx
"use client";

import { useForm, useFieldArray } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const memberSchema = z.object({
  name: z.string().min(1, "名前を入力してください"),
  email: z.string().email("正しいメールアドレスを入力してください"),
});

const schema = z.object({
  members: z.array(memberSchema).min(1, "メンバーを1人以上追加してください"),
});

type FormValues = z.infer<typeof schema>;

export function MemberForm() {
  const {
    register,
    control,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>({
    resolver: zodResolver(schema),
    defaultValues: { members: [{ name: "", email: "" }] },
  });

  const { fields, append, remove } = useFieldArray({
    control,
    name: "members",
  });

  const onSubmit = (data: FormValues) => {
    console.log("送信:", data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      {fields.map((field, index) => (
        // key には field.id を使う(data の id ではない)
        <div key={field.id} className="flex gap-2 rounded border p-3">
          <div className="flex-1 space-y-2">
            <input
              {...register(`members.${index}.name`)}
              placeholder="名前"
              className="w-full rounded border px-3 py-2 text-sm"
            />
            {errors.members?.[index]?.name && (
              <p className="text-xs text-red-600">{errors.members[index].name.message}</p>
            )}
            <input
              {...register(`members.${index}.email`)}
              placeholder="メールアドレス"
              className="w-full rounded border px-3 py-2 text-sm"
            />
            {errors.members?.[index]?.email && (
              <p className="text-xs text-red-600">{errors.members[index].email.message}</p>
            )}
          </div>
          <button
            type="button"
            onClick={() => remove(index)}
            disabled={fields.length === 1}
            className="self-start rounded border px-2 py-1 text-sm text-red-600 disabled:opacity-30"
          >
            削除
          </button>
        </div>
      ))}

      {errors.members?.root && (
        <p className="text-xs text-red-600">{errors.members.root.message}</p>
      )}

      <div className="flex gap-2">
        <button
          type="button"
          onClick={() => append({ name: "", email: "" })}
          className="rounded border px-4 py-2 text-sm"
        >
          メンバーを追加
        </button>
        <button
          type="submit"
          className="rounded bg-blue-600 px-4 py-2 text-sm text-white"
        >
          送信
        </button>
      </div>
    </form>
  );
}

ポイント

  • useFieldArrayfields には RHF が自動生成した id が付与される。key には必ず field.id を使う
  • append({ name: "", email: "" }) で末尾にフィールドを追加、remove(index) でインデックス指定で削除できる
  • register のフィールド名は members.${index}.name のように動的に指定する
  • Zod の z.array(memberSchema).min(1) で配列全体のバリデーション、各要素は memberSchema で検証される
  • 配列全体のエラーは errors.members?.root で参照する

注意点

useFieldArray の fields には RHF が生成した id が自動付与される。key には data の id ではなく fields の id を使う。Zod で配列の各要素にバリデーションを設定するには z.array(itemSchema) を使う。

関連サンプル