2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

shadcn/ui と React Hook Form で実装する動的Form

Posted at

目次

1. はじめに

shadcn/uiReact Hook Form を用いた動的な値を持つ Form の実装方法についてまとめました。下記の図のような、追加したり、削除したりできる項目を useForm で管理する方法を説明します。

usefield_form.png

2. 使用環境

Windows 11
Next.js : ^15.0.3
React : 18.2.0
React Hook Form : ^7.54.0
Tailwind CSS : ^3.4.1

3. 動的 Form の実装例

page.tsx
'use client';
import { PlusCircle, Trash2 } from 'lucide-react';

import { useFieldArray, useForm, useWatch } from 'react-hook-form';
import { z } from 'zod';

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';

export const itemSchema = z.object({
  id: z.number().int().positive(),
  value: z.string().nullable().optional(),
});

export const formSchema = z.object({
  items: z.array(itemSchema),
});

export type FormSchemaType = z.infer<typeof formSchema>;

const page = () => {
  const form = useForm<FormSchemaType>({
    defaultValues: {
      items: [{ id: 1, value: '' }],
    },
  });

  const { fields, append, remove } = useFieldArray({
    name: 'items',
    control: form.control,
  });

  const items = useWatch({
    name: 'items',
    control: form.control,
  });

  const addItem = () => {
    const newId = items.length > 0 ? Math.max(...items.map((item) => item.id)) + 1 : 1;
    append({ id: newId, value: '' });
  };

  const onSubmit = () => console.log('送信しました');

  return (
    <div className="App container my-8">
      <Form {...form}>
        <form className="space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
          <div className="space-y-2">
            <FormLabel>選択肢</FormLabel>
            {fields.map((item, index) => (
              <div key={item.id}>
                <FormField
                  name={`items.${index}.value`}
                  render={({ field }) => (
                    <FormItem>
                      <FormControl>
                        <div className="flex items-center space-x-2" key={item.id}>
                          <Input
                            aria-label={`選択肢 ${index + 1}`}
                            className="grow"
                            onChange={(e) => field.onChange(e.target.value)}
                            placeholder={`選択肢 ${index + 1}`}
                            type="text"
                            value={field.value ?? ''}
                          />
                          <Button
                            aria-label={`選択肢 ${index + 1} を削除`}
                            className="shrink-0"
                            onClick={() => remove(index)}
                            size="icon"
                            type="button"
                            variant="destructive"
                          >
                            <Trash2 className="size-4" />
                          </Button>
                        </div>
                      </FormControl>
                      <FormMessage />
                    </FormItem>
                  )}
                />
              </div>
            ))}
          </div>
          <div className="flex items-center justify-between pt-4">
            <Button className="mr-2 w-full" onClick={addItem} type="button" variant="outline">
              <PlusCircle className="mr-2 size-4" />
              選択肢を追加
            </Button>
            <Button
              className="ml-2 w-full"
              disabled={items.filter((item) => item.value?.trim() !== '').length < 2}
              type="submit"
            >
              '選択する'
            </Button>
          </div>
        </form>
      </Form>
    </div>
  );
};

export default page;

4. 動的 Form の実装方法

① Form の定義

今回は、id と value を持つ配列を値に持つ Form を例にして解説します。
最初に、formSchema を zod を使用して作成します。その後、useForm を使用して、react-hook-form を作成します。

export const itemSchema = z.object({
  id: z.number().int().positive(),
  value: z.string().nullable().optional(),
});

export const formSchema = z.object({
  items: z.array(itemSchema),
});

const form = useForm<FormSchemaType>({
  defaultValues: {
    items: [{ id: 1, value: "" }],
  },
});

② 動的 Form の操作を定義

useFieldArrayを使用して、動的 Form を操作できるようにします。① で定義した form の型とコンポーネントを登録するcontrolを指定して作成します。
以下の値を返します。

  • fields : items の値
  • append : items に値を追加する function
  • remove : items から値を削除する function
const { fields, append, remove } = useFieldArray({
  name: "items",
  control: form.control,
});

function に関しては、他にもいろいろあるので、以下を参照してください。

③ Form の実装

fields を map 関数を使って繰り返し、shadcn/uiの Form を作成します。
その際、FormFieldの render には、動的に値が変化する(ユーザの入力によって値が変わる)items.${index}.valueを指定します。

{
  fields.map((item, index) => (
    <div key={item.id}>
      <FormField
        name={`items.${index}.value`}
        render={({ field }) => (
          <FormItem>
            <FormControl>
              <div className="flex items-center space-x-2" key={item.id}>
                <Input
                  aria-label={`選択肢 ${index + 1}`}
                  className="grow"
                  onChange={(e) => field.onChange(e.target.value)}
                  placeholder={`選択肢 ${index + 1}`}
                  type="text"
                  value={field.value ?? ""}
                />
                <Button
                  aria-label={`選択肢 ${index + 1} を削除`}
                  className="shrink-0"
                  onClick={() => remove(index)}
                  size="icon"
                  type="button"
                  variant="destructive"
                >
                  <Trash2 className="size-4" />
                </Button>
              </div>
            </FormControl>
            <FormMessage />
          </FormItem>
        )}
      />
    </div>
  ));
}

④ Form 項目 の追加、削除機能を実装

項目の追加、削除機能は、② で定義した、append、remove を使用して実装します。
remove は index(削除したい値の箇所)、append は追加したい値を指定するだけで使用できるので簡単に扱えます。

<Button
aria-label={`選択肢 ${index + 1} を削除`}
className="shrink-0"
onClick={() => remove(index)}
size="icon"
type="button"
variant="destructive"
>
const addItem = () => {
  const newId =
    items.length > 0 ? Math.max(...items.map((item) => item.id)) + 1 : 1;
  append({ id: newId, value: "" });
};

<Button
  className="mr-2 w-full"
  onClick={addItem}
  type="button"
  variant="outline"
>
  <PlusCircle className="mr-2 size-4" />
  選択肢を追加
</Button>;
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?