🍆

shadcn/ui + Zod + react-hook-formの要素をコンポーネント化して見通しを良くする

2024/05/30に公開

shadcn/uiの公式ドキュメントで例が掲載されている通り、Zod + React Hook Formを使うとバリエーション付きのフォームが簡単に実装できます。

すごく簡単ではあるのですが、実装してみると縦長になってしまい見通しが悪くなってしまいます。(1項目だけでこの長さ。3項目作るとこの3倍ぐらいの長さになってしまいます。)

縦長になってしまう例を見る

このままでは扱いにくさが若干あるため、下記のように見通しが良好なフォームを作れるようにコンポーネント化していきます。

完成例(使うときの様子)を見る
完成例
export const Myform = () => {
  const form = useForm<FormData>({
    resolver: zodResolver(formSchema)
  });

  async function onSubmit(values: FormData) {
    console.log(values);
  }

  return (
    <Form {...form}>
      <form autoComplete="off">
        <InputElement label="お名前" name="name" placeholder="例: 山田太郎" control={form.control} />
        <InputElement label="会社名" name="companyName" placeholder="例: 株式会社HOGE" control={form.control} />
        <InputElement label="メールアドレス" name="email" type="email" placeholder="例: hoge@example.com" control={form.control} />
        <Button type="button" onClick={form.handleSubmit(onSubmit)}>
          送信する
        </Button>
    </Form>
  );
};

見通しが良いと感じるのはこちらだと思います!

あまりにも常識的すぎる知識であるが故に簡潔に書かれた記事が無かったため、本記事を投稿することにしました。(コード自体は単純ですが型周りの解決に苦労した。)

1. フォームのスキーマを作成する(バリエーションルールを作る)

_Form.tsx
"use client";

import { z } from "zod";
import { Control } from "react-hook-form";
import { FormControl, FormField, FormItem, FormLabel } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { HTMLInputTypeAttribute } from "react";

// フォームのスキーマを定義する
export const formSchema = z.object({
  name: z.string().trim().min(1, "お名前は必ず入力してください。"),
  companyName: z.string().trim().min(1, "会社名は必ず入力してください。"),
  email: z.string().trim().min(1, "メールアドレスは必ず入力してください。").email("メールアドレスは正しく入力してください。")
});

// フォームの型
export type FormData = z.infer<typeof formSchema>;

formSchema 変数にzodによるバリエーションルールを宣言します。
FormData をフォームの型を入れて export しておきます。

このファイルにそのまま記述していく形でinputのコンポーネントを作っていきます。

2. Input要素のコンポーネントを作る

_Form.tsx
interface InputProps {
  name: keyof FormData; // ここはanyでも良いかと。useFieldArrayで増減フォームを作るときにキー抽出に困ってしまうので...。(解決策分かる方教えてください!)
  type?: HTMLInputTypeAttribute;
  placeholder?: string;
  label: string;
  control: Control<FormData>;
}

export const InputElement = ({ label, name, type = "text", placeholder, control }: InputProps) => {
  return (
    <>
      <FormField
        control={control}
        name={name}
        render={({ field }) => (
          <>
            <FormItem>
              <FormLabel>{label}</FormLabel>
              <FormControl>
                <Input type={type} placeholder={placeholder} {...field} />
              </FormControl>
            </FormItem>
          </>
        )}
      />
    </>
  );
};

Control 型にFormData型を渡して control の型を定義しています。

InputProps でprops型を宣言します。
InputElement ではshadcn/uiを取り入れたInput要素を作っています。

3. 【使ってみよう】作ったコンポーネントでバリエーションが動くようにする

それでは作ったコンポーネントを呼び出して使ってみましょう。

_MyForm.tsx
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { Form } from "@/components/ui/form";
import { formSchema, FormData, InputElement } from "./_Form";
import { Button } from "@/components/ui/button";

export const MyForm = () => {
  const form = useForm<FormData>({
    resolver: zodResolver(formSchema)
  });

  async function onSubmit(values: FormData) {
    console.log(values);
  }

  return (
    <Form {...form}>
      <form autoComplete="off">
        <InputElement label="お名前" name="name" placeholder="例: 山田太郎" control={form.control} />
        <InputElement label="会社名" name="companyName" placeholder="例: 株式会社HOGE" control={form.control} />
        <InputElement label="メールアドレス" name="email" type="email" placeholder="例: hoge@example.com" control={form.control} />
        <Button type="button" onClick={form.handleSubmit(onSubmit)}>
          送信する
        </Button>
      </form>
    </Form>
  );
};

これで見通しの良いフォームが実装できました!


付録: クリックすると事前準備した任意のテキストをInputに入力してくれるボタンを実装する方法

上記のようによく入力する文言をボタンとして用意し、クリックでInputに入力しつつも、自由入力を受け付けるフォームも簡単に実装できます。

_Form.tsx
import { UseFormSetValue, useForm, UseFormWatch } from "react-hook-form";
import { Badge } from "@/components/ui/badge";

export function BudgeValue({ label, setValue, name }: { label: string; setValue: UseFormSetValue<FormData>; name: keyof FormData }) {
  return (
    <Badge variant="secondary" onClick={() => setValue(name, label, { shouldValidate: true })} className="cursor-pointer">
      {label}
    </Badge>
  );
}

setValue を使うことでフォームに任意の文字列を入力させることができます。

使い方
<BudgeValue name="companyName" label="A社" setValue={form.setValue} />
<BudgeValue name="companyName" label="B社" setValue={form.setValue} />
<BudgeValue name="companyName" label="C社" setValue={form.setValue} />

こんな感じで使います。
クリックするとlabelの値がそのまま挿入されるようになります。(A社、B社、C社というテキストが入る)

Discussion