目次
1. はじめに
shadcn/ui
と React Hook Form
を用いた動的な値を持つ Form の実装方法についてまとめました。下記の図のような、追加したり、削除したりできる項目を useForm
で管理する方法を説明します。
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 の実装例
'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>;