Open1
Shadcn/UI + React Hook Form + Zodでフォームを作成する

基本的にはShadcn/uiの公式サイトのドキュメントを参照すれば問題なく実装できます。
しかしながら、useStateと組み合わせると値の管理が難しく、実装に手間取ったため、注意点を記載します。
useStateで管理している値を、Formの初期値に設定する。
例えば、カレンダーアプリで日付を選択した際に、その日付をステート持っておき、その値をフォームに自動で入力する場面を考えます。
初期値を設定するには、useFormのsetValueを使います。
useEffectでプロップスが更新された際に、setValueにpropsを渡すことで、フォームに自動入力されるようにできます。
export const ScheduleForm = (props: any) => {
// ...
// プロップスで開始日と終了日のステートを受け取る
const { eventsStartDate, setEventsStartDate } = startDate;
const { eventsEndDate, setEventsEndDate } = endDate;
useEffect(() => {
form.setValue("start_date", eventsStartDate);
form.setValue("end_date", eventsEndDate);
}, [eventsStartDate]);
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(addEvent)} className="space-y-4">
<FormField
control={form.control}
name="start_date"
render={({ field }) => (
<FormItem className="w-full mr-2">
<FormLabel>開始日</FormLabel>
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"justify-start text-left font-normal",
!field.value && "text-muted-foreground"
)}
>
<CalendarIcon />
<span>{formatCaption(field.value)}</span>
</Button>
</PopoverTrigger>
<PopoverContent>
<Calendar
locale={ja}
mode="single"
selected={field.value}
onSelect={(date) => {
// 値を更新する際は、セットステートを一緒に更新する。
setEventsStartDate(date);
field.onChange(date);
}}
disabled={(date) =>
date > new Date() || date < new Date("1900-01-01")
}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="end_date"
render={({ field }) => (
<FormItem className="w-full mr-2">
<FormLabel>終了日</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={"outline"}
className={cn(
"justify-start text-left font-normal",
!field.value && "text-muted-foreground"
)}
>
<CalendarIcon />
<span>{formatCaption(field.value)}</span>
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent>
<Calendar
locale={ja}
mode="single"
selected={field.value}
onSelect={(date) => {
setEventsEndDate(date);
field.onChange(date); // Update the form state with the selected date
}}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
)
}
バリデーションがうまく動かない
hook FormとuseStateを使っていてバリデーションがうまく機能しない場合、以下の原因が考えられます。
const [eventsTitle, setEventsTitle] = useState<string>("")
...
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem className="w-md">
<FormLabel>タイトル</FormLabel>
<FormControl>
<Input
placeholder="title"
{...field}
value={eventsTitle}
onChange={(e) => setEventsTitle(e.target.value)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
上記のコードでは、Inputコンポーネントにvalue={eventsTitle}を設定していますが、これはReact Hook Formのfieldオブジェクトのvalueを上書きしてしまいます。fieldオブジェクトには、React Hook Formが管理する値が含まれているため、valueを直接指定するのではなく、field.valueを使用するべきです。
また、onChangeイベントでsetEventsTitleを呼び出す際に、field.onChangeも呼び出す必要があります。これにより、React Hook Formがフィールドの値を正しく管理できるようになります。
// 修正後
<Input
placeholder="title"
{...field}
value={field.value} // field.valueを使用
onChange={(e) => {
field.onChange(e.target.value); // field.onChangeを呼び出す
setEventsTitle(e.target.value); // 状態も更新
}}
/>
参考記事