🎃

shadcn/uiとReact Hook Formの連携を深堀りしてみる

2024/06/27に公開

案件でUIライブラリの shadcn/ui を利用しているのですが React Hook Form との連携に少しクセがあるので連携を深堀りしてみます。

https://ui.shadcn.com/

https://react-hook-form.com/

shadcn/ui は React Hook Form に依存しているのでフォームライブラリは基本的にはReact Hook Form一択になるかと思います。

FormFieldコンポーネントを使って連携

連携方法のベースはshadcn/uiの公式サイトで解説されています。

https://ui.shadcn.com/docs/components/form

公式サイトのコード

公式サイトでサンプルコードとして以下のような実装が提示されています。

"use client"

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

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

const formSchema = z.object({
  username: z.string().min(2, {
    message: "Username must be at least 2 characters.",
  }),
})


export default function ProfileForm() {
  // 1. Define your form.
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
    },
  })

  // 2. Define a submit handler.
  function onSubmit(values: z.infer<typeof formSchema>) {
    // Do something with the form values.
    // ✅ This will be type-safe and validated.
    console.log(values)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Username</FormLabel>
              <FormControl>
                <Input placeholder="shadcn" {...field} />
              </FormControl>
              <FormDescription>
                This is your public display name.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  );
}

shadcn/uiとReact Hook Formの連携部分だけ抽出

公式のサンプルコードはzodによるバリデーション等が考慮されたサンプルですが、shadcn/uiとReact Hook Formの連携部分だけ抽出すると以下のようになります。

"use client"

import { FieldValues, useForm } from "react-hook-form"
import { FormField } from "@/components/ui/form"
import { Input } from "@/components/ui/input"

export default function ProfileForm() {

  const form = useForm({
    defaultValues: {
      username: "",
    },
  })

  function onSubmit(values:FieldValues) {
    console.log(values)
  }

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <FormField
        control={form.control}
        name="username"
        render={({ field: { onChange } }) => (
          <Input onChange={onChange} />
        )}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

ポイントは<FormField>のrender propsでonChangeの関連付けを行っているところです。
これで入力内容を<FormField>で管理できるようになっているわけです。

        render={({ field: { onChange } }) => (
          <Input onChange={onChange} />
        )}

<FormField>が連携パートを担っているのが見えてきましたね。

FormFieldコンポーネントを使わずに連携

この挙動を深堀りするためにFormFieldコンポーネントを使わず実装してみましょう

<FormField>の実態はReact Hook Formの<FormProvider><Controller>を取り扱うためのコンポーネントです。

そのため<FormField>を使わない場合は<FormProvider><Controller>を利用すれば同様の処理が実装可能です。

"use client"

import { FormProvider, Controller, FieldValues, useForm, useFormContext } from "react-hook-form"
import { Input } from "@/components/ui/input"

const FormField = ({ children }: { children: React.ReactElement }) => {
  const form = useForm({
    defaultValues: {
      username: "",
    },
  })

  function onSubmit(values: FieldValues) {
    console.log(values)
  }

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <FormProvider {...form}>
        {children}
      </FormProvider>
      <button type="submit">Submit</button>
    </form>
  )
}

const InputFiled = () => {
  const { control } = useFormContext();
  return (
    <Controller
      control={control}
      name="username"
      render={({ field: { onChange } }) => <Input onChange={onChange} />}
    />
  );
};

export default function ProfileForm() {
  return (
    <FormField>
      <InputFiled />
    </FormField>
  );
}

かなり複雑になってしまいましたがReact Hook Formを使い慣れている方はこちらのほうがわかりやすいかもしれませんね。(<FormProvider />を使う機会はすくないと思いますが)

基本的には<FormField>を利用しますが、<FormField>を高度にカスタマイズしたい場合はReact Hook Formの<FormProvider><Controller>で再実装するのもありかもしれません。

株式会社トゥーアール

Discussion