Open1

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

kmkm

基本的にはShadcn/uiの公式サイトのドキュメントを参照すれば問題なく実装できます。
https://ui.shadcn.com/docs/components/form

しかしながら、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); // 状態も更新
  }}
/>

参考記事
https://zenn.dev/masa5714/scraps/0ae0c96ca20805