複数のチェックボックスがあるフォーム【React Hook FormとZod】

## はじめに
https://qiita.com/y-suzu/items/952d417f0853341a97df

こちらの記事ではReact Hook FormとZodの基本的な使い方をまとめました。
今回はもう少し応用的なフォームを作成してみます。
完成イメージと仕様、環境は以下の通りです。

![スクリーンショット 2023-05-03 15.44.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633215/8a724287-f455-c548-ba43-8fb01142053f.png)

- 仕様
    - 「名前」は1文字以上、必須
    - 「年齢」は任意
    - 「パスワード」は英大文字or英小文字or数字を使って8文字以上、必須、onBlur時にバリデーションチェック
    - 「好きな食べ物」はチェックボックス、任意
    - 「その他の好きな食べ物」は「その他」がチェックされているときのみ必須
    - バリデーションが成功したら入力値を取得できる
- 環境
    - Next.js 13.3.4
    - react-hook-form 7.43.9
    - zod 3.21.4
    - chakra-ui 2.6.0

## コード例

```tsx:use-applied-form-top.tsx
import { useForm } from "react-hook-form";
import {
  appliedFormSchema,
  AppliedFormSchemaType,
} from "@/features/applied-form/schema";
import { zodResolver } from "@hookform/resolvers/zod";

export const useAppliedFormTop = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
    trigger,
    control,
  } = useForm<AppliedFormSchemaType>({
    resolver: zodResolver(appliedFormSchema),
  });

  const onSubmit = (data: AppliedFormSchemaType) => {
    console.log(data);
  };

  return {
    register,
    onSubmit: handleSubmit(onSubmit),
    errors,
    trigger,
    control,
  };
};
```

- `trigger`はバリデーションのタイミングを手動で設定するために使います
- `control`は後述する`Controller`コンポーネントを定義する際に必要です

```tsx:applied-form-top.tsx
import {
  Container,
  Heading,
  FormControl,
  FormLabel,
  FormErrorMessage,
  Input,
  Button,
  CheckboxGroup,
  Checkbox,
} from "@chakra-ui/react";
import { useAppliedFormTop } from "@/features/applied-form/hooks";
import { Controller } from "react-hook-form";

const foods = [
  {
    id: "sushi",
    label: "寿司",
  },
  {
    id: "curry",
    label: "カレー",
  },
  {
    id: "pizza",
    label: "ピザ",
  },
  {
    id: "other",
    label: "その他",
  },
];

export const AppliedFormTop = () => {
  const { register, onSubmit, errors, trigger, control } = useAppliedFormTop();
  return (
    <Container mt={12}>
      <Heading as={"h2"} mb={8}>
        応用的なフォーム
      </Heading>
      <form onSubmit={onSubmit}>
        <FormControl isInvalid={!!errors.name}>
          <FormLabel htmlFor={"name"}>名前</FormLabel>
          <Input type={"text"} id={"name"} {...register("name")} />
          <FormErrorMessage>
            {errors.name && errors.name.message}
          </FormErrorMessage>
        </FormControl>

        <FormControl mt={8}>
          <FormLabel htmlFor={"age"}>年齢</FormLabel>
          <Input type={"text"} id={"age"} {...register("age")} />
        </FormControl>

        <FormControl isInvalid={!!errors.password} mt={8}>
          <FormLabel htmlFor={"password"}>パスワード</FormLabel>
          <Input
            type={"password"}
            id={"password"}
            {...register("password")}
            onBlur={() => trigger("password")}
          />
          <FormErrorMessage>
            {errors.password && errors.password.message}
          </FormErrorMessage>
        </FormControl>

        <FormControl mt={8}>
          <FormLabel>好きな食べ物</FormLabel>
          <Controller
            name="foods"
            control={control}
            render={({ field }) => {
              return (
                <CheckboxGroup {...field} defaultValue={[]}>
                  {foods.map((food) => {
                    return (
                      <Checkbox key={food.id} value={food.id} mr={8}>
                        {food.label}
                      </Checkbox>
                    );
                  })}
                </CheckboxGroup>
              );
            }}
          />
        </FormControl>

        <FormControl isInvalid={!!errors.other} mt={8}>
          <FormLabel htmlFor={"other"}>その他の好きな食べ物</FormLabel>
          <Input type={"text"} id={"other"} {...register("other")} />
          <FormErrorMessage>
            {errors.other && errors.other.message}
          </FormErrorMessage>
        </FormControl>

        <Button type="submit" mt={8}>
          送信
        </Button>
      </form>
    </Container>
  );
};
```

- `trigger(名前)`とすることで、その入力のバリデーションをチェックできます
    - 今回はパスワードのinputのonBlur時に実行するようにしています
- `Controller`コンポーネントについて
    - 外部のライブラリやUIコンポーネントを使用して、フォームを作成、制御する際に使用します
    - `name`にはフィールドの名前、`control`にはuseFormから受け取った`control`オブジェクト、`render`にはフォームフィールドを描画するためのコンポーネントを渡します
    - また、引数の`field`にはフォームフィールドに関する情報が含まれています

```ts:index.ts
import { z } from "zod";

export const appliedFormSchema = z
  .object({
    name: z.string().min(1, { message: "名前を入力してください" }),
    age: z.string().nullable(),
    password: z
      .string()
      .min(8, { message: "8桁以上のパスワードを入力してください" })
      .regex(/^[a-zA-Z0-9]+$/, {
        message: "英大文字、英小文字、数字で入力してください",
      }),
    foods: z.array(z.string()),
    other: z.string().nullable(),
  })
  .refine((data) => !data.foods.includes("other") || !!data.other, {
    message: "その他の好きな食べ物を入力してください",
    path: ["other"],
  });

export type AppliedFormSchemaType = z.infer<typeof appliedFormSchema>;
```

- `refine()`について
    - `refine()`を使うことで独自のバリデーションロジックを設定できます
    - 第1引数にバリデーション関数を設定し、期待する条件を書きます
    - 第2引数にいくつかのオプションを設定します（一部抜粋）
        - `message`：エラーメッセージ
        - `path`：エラー扱いにするフィールド


## 参考記事
https://react-hook-form.com/api/useform/trigger/

https://react-hook-form.com/api/useform/control/

https://react-hook-form.com/api/usecontroller/controller/

https://zenn.dev/vbbvbbvbbv/articles/42e57653e31ee3

https://zod.dev/?id=refine

https://zenn.dev/s_takashi/articles/99e18f74995a87

## 採用のお知らせ
株式会社Relicでは、エンジニア・デザイナーを積極的に採用中です。
またRelicでは、地方拠点がありますので、U・Iターンも大歓迎です！🙌
少しでもご興味がある方は、Relic採用サイトからエントリーください！

https://relic.co.jp/recruit?utm_source=qiita&utm_medium=article&utm_campaign=qiita_article

https://relic.co.jp/categories/job_4?utm_source=qiita&utm_medium=article&utm_campaign=qiita_article