react-hook-form と zod による、基本的なバリデーション付きフォーム

## はじめに

本記事では、React 用のフォームライブラリ「react-hook-form」と「zod」の使い方を解説します。react-hook-form と zod を使えば、最小限のコードでバリデーション付きフォームを実装できます。

本記事を読むことで、以下のことが学べます。

- react-hook-form の基本的な使い方
- zod を使った基本的なスキーマ定義とバリデーション
  - refine を使った高度なバリデーション
- TypeScriptでの記述時の型のポイント

※ コードのみの説明にするため、[Water.css](https://watercss.kognise.dev/) による最低限のデザインになっています。

## react-hook-form のみでシンプルなフォームを作る

まずは、名前とメールアドレスだけの簡単なフォームを作ってみましょう。react-hook-form では、`useForm`フックを使ってフォームの状態を管理します。

![Screenshot 2024-03-25 at 9.08.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10962/9c4d1ff4-d5d7-dfa4-47f2-44cda30f0916.png)


```tsx
import { useForm } from "react-hook-form";

export default function MyForm() {
  const { register, handleSubmit } = useForm();

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input type="text" placeholder="名前" {...register("name")} />
      <input type="email" placeholder="メールアドレス" {...register("email")} />
      <button type="submit">送信</button>
    </form>
  );
}
```

ここでのポイントは以下の 3 つです。

- `useForm`フックを使ってフォームの状態を管理
- `register`関数でフォームのフールドを登録
- `handleSubmit`関数で送信時の処理を定義

`useForm`はフォーム全体の状態を管理するためのフックで、バリデーションやエラーハンドリングなどの機能を提供します。

`register`は、フォーム内の各フィールドを`useForm`に登録するための関数です。これにより、react-hook-form がフィールドの状態を追跡できるようになります。

`handleSubmit`は、フォーム送信時に呼び出される関数を定義するためのユーティリティ関数です。ここでは、フォームのデータを受け取って処理する`onSubmit`関数を渡しています。

以上が、react-hook-form を使った基本的なフォームの実装例です。

## デフォルト値とリセット機能の追加

次に、メールアドレスのデフォルト値を設定し、リセットボタンを追加してみましょう。

![Screenshot 2024-03-25 at 9.13.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10962/d0547077-d2b5-0577-f237-fdfecf4e4a4e.png)


```tsx
import { useForm } from "react-hook-form";

export function MyForm(props) {
  const { defaultValues } = props;
  const { register, handleSubmit, reset } = useForm({
    defaultValues,
  });

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input type="text" placeholder="名前" {...register("name")} />
      <input type="email" placeholder="メールアドレス" {...register("email")} />
      <button type="submit">送信</button>
      <button type="button" onClick={() => reset()}>
        リセット
      </button>
    </form>
  );
}

// MyFormのpropsにdefaultValuesを渡します
export default function App() {
  return (
    <MyForm
      defaultValues={{
        email: "test@example.com",
      }}
    />
  );
}

```

`useForm`フックには、`defaultValues`オプションを指定することで、各フィールドのデフォルト値を設定できます。ここでは、`email`フィールドに初期値を与えています。

また、`reset`関数を使うと、フォームを初期状態にリセットすることができます。ここではリセットボタンを押すと、`defaultValues`で指定した値に戻ります。

## zod によるバリデーションの追加

基本的にフォームには、ユーザ入力のバリデーションが必須です。react-hook-form では、`resolver`オプションを使って、柔軟なバリデーションを行えます。

ここでは、zod というスキーマ定義ライブラリを使って、フィールドのバリデーションを追加します。

![Screenshot 2024-03-25 at 9.25.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10962/beb48979-91a0-8de0-d744-68d37377d27d.png)

![Screenshot 2024-03-25 at 9.25.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10962/9a0752bb-db13-28bb-c0d7-913f140dc391.png)


```tsx
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";

// zodを使って、フィールドのスキーマを定義します
const schema = z.object({
  name: z.string().min(1, "名前は必須です"),
  email: z.string().email("正しいメールアドレスを入力してください"),
  age: z.number().min(18, "18歳以上である必要があります"),
});

export default function MyForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    // zodResolver関数を使って、バリデーション用のリゾルバを作成し、
    // そのまま作成したリゾルバを渡します
    resolver: zodResolver(schema),
  });

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input type="text" placeholder="名前" {...register("name")} />
      {errors.name && <span style={errorStyle}>{errors.name.message}</span>}

      <input type="email" placeholder="メールアドレス" {...register("email")} />
      {errors.email && <span style={errorStyle}>{errors.email.message}</span>}

      <input
        type="number"
        placeholder="年齢"
        {...register("age", { valueAsNumber: true })}
      />
      {errors.age && <span style={errorStyle}>{errors.age.message}</span>}

      <button type="submit">送信</button>
    </form>
  );
}

const errorStyle = {
  color: "red",
};
```

zod を使ったバリデーションは以下のようなステップで行います。

1. zod を使って、フィールドのスキーマを定義
2. `zodResolver`関数を使って、バリデーション用のリゾルバを作成
   - リゾルバは、フォームの入力値をチェックするためのバリデーションロジックを定義する関数です。`zodResolver`関数は、zod で定義したスキーマを受け取り、それに対応するリゾルバ(バリデーション関数)を作成します
3. `useForm`フックの`resolver`オプションに、作成したリゾルバを渡す

zod のスキーマ定義は非常に直感的で、必須チェックや型チェックなどを簡単に行えます。

また、バリデーションエラーが発生した場合は、`formState.errors`オブジェクトからエラーメッセージを取得して表示できます。

## refine を使用した複雑なバリデーション

refine を使用したもう少し複雑なバリデーションの例を見てみましょう。

![Screenshot 2024-03-25 at 9.23.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10962/cdd6556d-245f-5b0e-c73d-e4ee0c1aece1.png)

![Screenshot 2024-03-25 at 9.24.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10962/9f8164cd-28c6-32ae-07b8-574ddb9975cd.png)


```jsx
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";

const schema = z
  .object({
    password: z
      .string()
      .min(8, "パスワードは8文字以上で入力してください")
      .regex(/^[a-zA-Z0-9]+$/, "パスワードは英数字のみで入力してください"),
    confirmPassword: z.string(),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: "パスワードが一致しません",
    path: ["confirmPassword"],
  });

export default function MyForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: zodResolver(schema),
  });

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        type="password"
        placeholder="パスワード"
        {...register("password")}
      />
      {errors.password && (
        <span style={errorStyle}>{errors.password.message}</span>
      )}

      <input
        type="password"
        placeholder="パスワード（確認用）"
        {...register("confirmPassword")}
      />
      {errors.confirmPassword && (
        <span style={errorStyle}>{errors.confirmPassword.message}</span>
      )}

      <button type="submit">送信</button>
    </form>
  );
}

const errorStyle = {
  color: "red",
};
```

ここでは、`password`と`confirmPassword`という 2 つのパスワード入力欄を設け、それらが一致することを検証しています。

zod の`refine`メソッドを使うと、複数のフィールドを跨いだバリデーションを行えます。この例では、`password`と`confirmPassword`が等しいかどうかをチェックし、一致しない場合はエラーを設定しています。

## TypeScript のサンプルコード

上述の zod のバリデーション付きフォームを TypeScript で書き直してみましょう。

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

// フィールドのスキーマを zod で定義
const schema = z.object({
  name: z.string().min(1, "名前は必須です"),
  email: z.string().email("正しいメールアドレス形式で入力してください"),
  age: z.coerce.number().min(18, "18歳以上である必要があります"),
});

// フォームの入力値の型を上述のスキーマから作成
type FormValues = z.infer<typeof schema>;

export default function MyForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>({
    resolver: zodResolver(schema),
  });

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input type="text" placeholder="名前" {...register("name")} />
      {errors.name && <span>{errors.name.message}</span>}

      <input type="email" placeholder="メールアドレス" {...register("email")} />
      {errors.email && <span>{errors.email.message}</span>}

      <input type="number" placeholder="年齢" {...register("age")} />
      {errors.age && <span>{errors.age.message}</span>}

      <button type="submit">送信</button>
    </form>
  );
}
```

まず、`z.infer<typeof schema>` を使って、zod のスキーマから TypeScript の型を推論します。
この `FormValues` 型を `useForm` に渡すことで、フォームの入力値の型を指定できます。

そして `coerce` メソッドを使うことで、文字列型の入力値を数値型に変換します。

## まとめ

react-hook-form は、シンプルで使いやすく、かつ柔軟性の高いフォームライブラリです。zod もシンプルで使いやすく、かつ柔軟性の高いバリデーションライブラリです。本記事では、一部の機能を紹介しましたが、react-hook-form と zod にはまだまだ多くの機能があります。

以下に公式ドキュメントを参照しておきます。

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

https://zod.dev/

# 執筆時点の各種バージョン

- "react": "18.2.0",
- "react-dom": "18.2.0",
- "react-hook-form": "7.51.0",
- "zod": "3.22.4"
- "@hookform/resolvers": "3.3.4"
- "typescript": "5.2.2"