React Hook Form + Zod で簡単フォーム作成＆バリデーション

# はじめに
ReactでForm実装といえば **React Hook Form** !!

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

でもvalidationがちょっと弱い・・・。

![cap2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/2ad03690-bde0-52c9-ef37-6f7da46099bd.png)


validationといえば **Zod** !!

https://github.com/colinhacks/zod

どっちも実装したことがない・・・。
やってみます！！！

![cap1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/43b3f390-ea8c-b535-3e6d-c77003b22678.png)


# もくじ
1. プロジェクト用意
1. React Hook Formをいろいろさわる
1. Zodに入門してみる
1. React Hook Form + Zod で簡単フォーム作成＆バリデーション

## プロジェクト用意
### vite + Reactプロジェクト用意
```bash
pnpm create vite zod-hello-world --template react-ts
```

プロジェクトを作成します。

```bash
cd zod-hello-world
```

移動して・・・。

```bash
pnpm install
```

関連パッケージインストールします。

```bash
pnpm run dev
```

はろーわします。

![cap3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/e9bc061a-2d6c-7c06-1bbd-684da2713d1a.png)

良い感じですね！

:::note info
viteやpnpmって何？って方は以下の記事もぜひ読んでみてください！（宣伝）
:::

https://qiita.com/suiwave/items/879ceafdde7d9dd51c1c

## React Hook Form をいろいろさわる

では、まずは React Hook Form でFormを実装してみます！！

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

### React Hook Form ライブラリ用意
```bash
pnpm add react-hook-form
```

ライブラリをインストールします。

```bash
Packages: +1
+
Progress: resolved 229, reused 191, downloaded 0, added 1, done

dependencies:
+ react-hook-form 7.53.1

Done in 1.5s
```

無事おわりました。

### React Hook Form をちょろっと触ってみる
以下のサンプルコードをいじって、React Hook Formをふんわり理解します。

https://www.react-hook-form.com/get-started#Quickstart

コードをコピペして問題なく動いたら、コードを確認してライブラリの仕様を確認します。

#### コンポーネント準備

```diff
zod-hello-world
  ├── node_modules
  ├── public
  ├── src
  │   ├── assets
+ │   └── components           // 新規作成
+ │       └── VanillaForm.tsx  // 新規作成。React Hook Form を使ってみる
  ├── App.css
+ ├── App.tsx                  // VanillaForm をレンダリングするよう修正
  ├── index.css
  ├── index.html
  ├── main.tsx
  ├── package.json
  ├── pnpm-lock.yaml
  ├── README.md
  ├── tsconfig.app.json
  ├── tsconfig.json
  ├── tsconfig.node.json
  ├── vite.config.ts
  └── vite-env.d.ts
```

React Hook Form を使ってみるコンポーネントを1つ作成します。
また、そのコンポーネントをレンダリングするようApp.tsxを修正します。

#### src/components/VanillaForm.tsx 作成
get startedページのコンポーネントを翻訳しつつコピペします。

```components/VanillaForm.tsx
import { useForm, SubmitHandler } from "react-hook-form";

type Inputs = {
    example: string,
    exampleRequired: string,
};

export default function App() {
    const { register, handleSubmit, watch, formState: { errors } } = useForm<Inputs>();
    const onSubmit: SubmitHandler<Inputs> = data => console.log(data);

    console.log(watch("example")) // 入力値を監視する - 入力項目の名前を渡すことで監視可能

    return (
        /* "handleSubmit"はonSubmitを呼び出す前に入力値のバリデーションを行います */
        <form onSubmit={handleSubmit(onSubmit)}>
            {/* registerフォーム関数を呼び出して入力フィールドをhookに登録します */}
            <input defaultValue="test" {...register("example")} />

            {/* required属性やその他の標準HTMLバリデーションルールを含めることができます */}
            <input {...register("exampleRequired", { required: true })} />
            {/* フィールドのバリデーションが失敗した場合にエラーを表示します */}
            {errors.exampleRequired && <span>この項目は必須です</span>}

            <input type="submit" />
        </form>
    );
}
```

#### src/App.tsx 修正
差分多すぎるので修正後のソースを記載します。
レンダリングしてるだけですね。

```src/App.tsx
import VanillaForm from "./components/VanillaForm"

function App() {
  return (
    <>
      <VanillaForm />
    </>
  )
}

export default App
```

#### localhostで表示を確認

![cap4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/1fa04b69-3e2b-b801-677d-1f09e06b3e33.png)

寂しすぎますが、良い感じですね。

![cap5.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/c4678005-76f3-f98d-a5e3-166f78dd5768.png)

submitするとしかられました。嬉しいですね。

#### サンプルコードを読み解く
コードを再掲します。上から見ていきます

```components/VanillaForm.tsx
import { useForm, SubmitHandler } from "react-hook-form";

type Inputs = {
    example: string,
    exampleRequired: string,
};

export default function App() {
    const { register, handleSubmit, watch, formState: { errors } } = useForm<Inputs>();
    const onSubmit: SubmitHandler<Inputs> = data => console.log(data);

    console.log(watch("example")) // 入力値を監視する - 入力項目の名前を渡すことで監視可能

    return (
        /* "handleSubmit"はonSubmitを呼び出す前に入力値のバリデーションを行います */
        <form onSubmit={handleSubmit(onSubmit)}>
            {/* registerフォーム関数を呼び出して入力フィールドをhookに登録します */}
            <input defaultValue="test" {...register("example")} />

            {/* required属性やその他の標準HTMLバリデーションルールを含めることができます */}
            <input {...register("exampleRequired", { required: true })} />
            {/* フィールドのバリデーションが失敗した場合にエラーを表示します */}
            {errors.exampleRequired && <span>この項目は必須です</span>}

            <input type="submit" />
        </form>
    );
}
```

見ていきます

```components/VanillaForm.tsx
// L1~
import { useForm, SubmitHandler } from "react-hook-form";
```

ライブラリからいろいろimportしてますね。
これらをどう使うかはあとで確認します。

```components/VanillaForm.tsx
// L3~
type Inputs = {
    example: string,
    exampleRequired: string,
};
```

Form要素のTypeを定義しています。型最高！！！

```components/VanillaForm.tsx
// L8~
export default function App() {
    const { register, handleSubmit, watch, formState: { errors } } = useForm<Inputs>();
```

importしたuseFormを実行しています。
ジェネリクスにInput型を渡してますね。
色々もらってるものがどんなものか確認していきましょう。

```components/VanillaForm.tsx
// L12~
    console.log(watch("example")) // 入力値を監視する - 入力項目の名前を渡すことで監視可能
```

```useForm``` から受け取った ```watch``` を使用しています。
コメントを信じて適当に値を入力してみます。

![cap6.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/99d6f173-2bce-0b11-c499-175853a42cc7.png)

watchできてますね！引数なしだとForm全体をwatchするようです。なるほど。

```components/VanillaForm.tsx
// L16~
    return (
        /* "handleSubmit"はonSubmitを呼び出す前に入力値のバリデーションを行います */
        <form onSubmit={handleSubmit(onSubmit)}>
```

続くformタグでは、```useForm``` から受け取った ```handleSubmit``` を使用しています。
```handleSubmit``` を使用することで、バリデーションを```onSubmit``` 前に行ってくれるらしいです。

![cap7.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/3fd453d5-301d-eba7-0a8e-3da0e0e834b7.png)

関数の引数を確認すると、第二引数に ```onInvalid``` を指定できるようです。
```handleSubmit``` はsubmit前にvalidationしてくれて、
その結果に応じて第一引数の関数 or 第二引数の関数を実行してくれます。便利！

コードを修正して確認します。
```diff_tsx
- import { useForm, SubmitHandler } from "react-hook-form";
+ import { useForm, SubmitHandler, SubmitErrorHandler } from "react-hook-form";

type Inputs = {
    example: string,
    exampleRequired: string,
};

export default function App() {
    const { register, handleSubmit, watch, formState: { errors } } = useForm<Inputs>();
-    const onSubmit: SubmitHandler<Inputs> = data => console.log(data);
+    const onSubmit: SubmitHandler<Inputs> = data => console.log('submit', data);
+    const onInvalid: SubmitErrorHandler<Inputs> = data => console.log('invalid', data);

    console.log(watch("example")) // 入力値を監視する - 入力項目の名前を渡すことで監視可能

    return (
        /* "handleSubmit"はonSubmitを呼び出す前に入力値のバリデーションを行います */
-        <form onSubmit={handleSubmit(onSubmit)}>
+        <form onSubmit={handleSubmit(onSubmit, onInvalid)}>
```

invalid時の関数の型をimportし、```handleSubmit``` 関数に渡します。

良い感じですね！

Valid時は関数内でFormが参照でき、
Invalid時は関数内でエラー情報が参照できてそうです

次はinputタグを作成しているところを見て見ます。

```components/VanillaForm.tsx
// L17~
   {/* registerフォーム関数を呼び出して入力フィールドをhookに登録します */}
   <input defaultValue="test" {...register("example")} />
```

register関数の使い方がなんか奇妙ですね。

これを紐解くために、そもそもregister関数が何を返すか確認します。

```diff_tsx
    console.log(watch("example")) // 入力値を監視する - 入力項目の名前を渡すことで監視可能
+   console.log(register("example"))
    return (
```

![cap8.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/2c43d0e1-d900-167f-56d7-bf6def7b5cca.png)

```name``` や ```onchange``` が詰まっているオブジェクトが返却されるようです。
**inputに渡したいpropsがオブジェクトになっている**にように見えませんか？

豆知識なのですが、Reactのpropsはこのような渡し方ができます

```jsx
const props = { hoge: 'aaa', fuga: 'bbb' }
<Component hoge={props.hoge} fuga={props.huga} />
        //  ↓↓↓
<Component {...props} />
```

今回のケースに置き換えるとこうですね。

```jsx
const props = register("example")
//          = {name: 'example', onChange: ƒ, onBlur: ƒ, ref: ƒ}
<input name={props.name} onChange={props.onChange}  onBlur={props.onBlur}  ref={props.ref} />
        //  ↓↓↓
<input {...props} />
```

つまり、このコードはinputタグをexampleに対応させるコードなんですね。

```components/VanillaForm.tsx
// L17~
   {/* registerフォーム関数を呼び出して入力フィールドをhookに登録します */}
   <input defaultValue="test" {...register("example")} />
```

※参考

https://koshiro54600.hatenablog.com/entry/2022/10/03/130115

最後に、エラーの表示方法を確認します。

```components/VanillaForm.tsx
// L9~
    const { register, handleSubmit, watch, formState: { errors } } = useForm<Inputs>();

// L22~
   {/* フィールドのバリデーションが失敗した場合にエラーを表示します */}
   {errors.exampleRequired && <span>この項目は必須です</span>}
```

```useFrom``` で返却された```errors``` にエラー情報が入っていて、そこで分岐する感じですね。

![cap9.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/39cf721a-5396-2534-4dd1-c42b3da017d4.png)

errorsには型情報に応じた各メンバーが定義されていそうです。
?がついているので、エラー時のみtruthyになりそうですね。

![cap10.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/a3686ac7-1e3d-07d5-cd4c-85a1bc166761.png)

エラー情報も全部hooksが管理してくれているのは直感的で便利そう！

なんとなくReact Hook Form の雰囲気は感じ取れたので、次はZodに入門してみます。

## Zodに入門してみる

### Zod ライブラリ用意

addしていきますー

```bash
pnpm add zod
```

```bash
Packages: +1
+
Progress: resolved 230, reused 192, downloaded 0, added 1, done

dependencies:
+ zod 3.23.8

Done in 1.5s
```

おっけーですね。

また、ローカルでさくっとtsファイルを実行するために、tsx というライブラリも追加します。

```bash
pnpm add -D tsx
```

```bash
Packages: +5
+++++
Progress: resolved 259, reused 198, downloaded 0, added 5, done

devDependencies:
+ tsx 4.19.1

Done in 4.2s
```

tsxってなに？という方は以下の記事がおすすめです！めっちゃわかりやすい

https://qiita.com/ssssota/items/115a906e960bcfabb46b

では、以下のサンプルコードをいじって、Zodをふんわり理解します。

https://www.npmjs.com/package/zod

今回はReact Hook Form とつなげるので、以下のようなFormを作ることをゴールにおきます。

```
名前：必須。
年齢：必須。20歳以上。80歳以下。
メールアドレス：必須。メアド形式必須。
パスワード：必須。8文字以上で英数字のみで構成されていること。
確認用パスワード：必須。パスワードと等しいこと。
```

やっていきましょう！

### Zod 入門

```diff
zod-hello-world
  ├── node_modules
  ├── public
  ├── src
  │   ├── assets
  │   └── components
  │       └── VanillaForm.tsx
  ├── App.css
  ├── App.tsx
+ ├── hello_zod.ts             // 新規作成。zodをつかってみる
  ├── index.css
  ├── index.html
  ├── main.tsx
  ├── package.json
  ├── pnpm-lock.yaml
  ├── README.md
  ├── tsconfig.app.json
  ├── tsconfig.json
  ├── tsconfig.node.json
  ├── vite.config.ts
  └── vite-env.d.ts
```


#### Zod 要素の定義

zodでは以下のようなメソッドでプロパティの型を定義します。
```src/hello_zod.ts
import { z } from "zod";

// 名前フィールド
const name = z.string();

// 年齢フィールド（20-80歳）
const age = z.number();

// メールアドレスフィールド
const email = z.string();

// パスワードフィールド（8文字以上、英数字のみ）
const password = z.string();

// 確認用パスワードフィールド
const confirmPassword = z.string();
```

プリミティブな型やundefined、Nullなどもサポートされています。

![cap11.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/2d8cca3b-e783-8e2b-2f0d-692f5210e96f.png)

ドキュメントより引用。

ただ、これらのフィールドを個々の変数で扱うのは微妙なので、オブジェクト化します。

#### Zod オブジェクトの定義
z.object()で引数にオブジェクトを与えてあげるだけでオブジェクトを定義できます！

```src/hello_zod.ts
import { z } from "zod";

// 名前フィールド
const name = z.string();

// 年齢フィールド（20-80歳）
const age = z.number();

// メールアドレスフィールド
const email = z.string();

// パスワードフィールド（8文字以上、英数字のみ）
const password = z.string();

// 確認用パスワードフィールド
const confirmPassword = z.string();

const user = z.object({
    name,
    age,
    email,
    password,
    confirmPassword,
});
```
ではFormで扱いたいオブジェクトを定義できたので、validationしていきます！

#### Zod parse、safeParse
Zodでは、```parse``` , ```safeParse``` というメソッドでvalidationをおこなうことができます。
```parse``` はvalidation失敗時にErrorをthrowするメソッドです。

![cap13.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/0cd9e733-74d8-a0ea-1532-b43e08bb45f5.png)

```safeParse``` はvalidation失敗時にErrorをthrowせず、結果を返却するメソッドです。

![cap14.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/5ce4497f-4188-69a7-dc6b-fe093f56f8f1.png)

今回は使いやすいsafeParseを使っていきます。

```diff_typescript
    import { z } from "zod";
    
    // 名前フィールド
    const name = z.string();
    
    // 年齢フィールド（20-80歳）
    const age = z.number();
    
    // メールアドレスフィールド
    const email = z.string();
    
    // パスワードフィールド（8文字以上、英数字のみ）
    const password = z.string();
    
    // 確認用パスワードフィールド
    const confirmPassword = z.string();
    
    const user = z.object({
        name,
        age,
        email,
        password,
        confirmPassword,
    });
+   console.log(user.safeParse({}))
```

舐めすぎてますが、なにもないオブジェクトをUserスキーマでParseしてみます

```powershell
pnpm tsx .\src\hello_zod.ts
```

結果が出ます。

```powershell
{ success: false, error: [Getter] }
```

parseに失敗して、errorが発行されています。

errorの内容は、```safeParse({}).error?.issues``` でアクセスできるので、コードを修正してエラー内容を確認します。

```diff_typescript
-   console.log(user.safeParse({}))
+   console.log(user.safeParse({}).error?.issues)
```

実行するとこんな感じのログがでます

```
[
  {
    code: 'invalid_type',
    expected: 'string',
    received: 'undefined',
    path: [ 'name' ],
    message: 'Required'
  },
  {
    code: 'invalid_type',
    expected: 'number',
    received: 'undefined',
    path: [ 'age' ],
    message: 'Required'
  },
  {
    code: 'invalid_type',
    expected: 'string',
    received: 'undefined',
    path: [ 'email' ],
    message: 'Required'
  },
  {
    code: 'invalid_type',
    expected: 'string',
    received: 'undefined',
    path: [ 'password' ],
    message: 'Required'
  },
  {
    code: 'invalid_type',
    expected: 'string',
    received: 'undefined',
    path: [ 'confirmPassword' ],
    message: 'Required'
  }
]
```

各フィールドで、要素が存在しないエラーが発生していますね。いい感じ

逆に、parseをpassする引数で実行してみます。

```diff_typescript
-   console.log(user.safeParse({}).error?.issues)
+   console.log(user.safeParse({
+       name: 'name',
+       age: 999,
+       email: 'email',
+       password: 'password',
+       confirmPassword: 'confirmPassword',
+   }))
```

ログを確認します。

```
{
  success: true,
  data: {
    name: 'name',
    age: 999,
    email: 'email',
    password: 'password',
    confirmPassword: 'confirmPassword'
  }
}
```

```success: true,``` が返却されてます！いい感じですね。

現状では、stringやnumberなどの型チェックしか実装できていません。
次からは、その他validationを追加していきます！！！

#### Zod validation追加
zodでは、このようにメソッド形式でわかりやすくvalidationを追加できます。

では、Userに以下のvalidationを追加していきます！！！

```
名前：必須。
年齢：必須。20歳以上。80歳以下。
メールアドレス：必須。メアド形式必須。
パスワード：必須。8文字以上で英数字のみで構成されていること。
確認用パスワード：必須。パスワードと等しいこと。
```

##### Zod 必須化
zod.string()関数は第一引数にオブジェクトを受け取ります。

![cap16.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/4b81a0e5-0574-77c0-7194-8fa308227403.png)

このinvalid_type_error や required_errorにエラーメッセージを追加することで、簡単にエラーメッセージが実装できます。

```diff_typescript
-   const name = z.string();
+   const name = z.string({ required_error: "名前は必須です", });
+   console.log(name.safeParse(undefined).error?.issues)
```

tsxで実行

```
[
  {
    code: 'invalid_type',
    expected: 'string',
    received: 'undefined',
    path: [],
    message: '名前は必須です'
  }
]
```

良い感じですね！

つぎは年齢のvalidationを実装します！

```
名前：必須。
年齢：必須。20歳以上。80歳以下。
メールアドレス：必須。メアド形式必須。
パスワード：必須。8文字以上で英数字のみで構成されていること。
確認用パスワード：必須。パスワードと等しいこと。
```

##### Zod min, max
```min```, ```max``` 関数で実装できます。

![cap15_num.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/ce720d9d-d98d-ac1c-30ec-8fc63ec73d38.png)

```diff_typescript
-  const age = z.number();
+  const age = z.number().min(20, '年齢は20歳以上である必要があります').max(80, '年齢は80歳以下である必要があります');
+  console.log(age.safeParse(1).error?.issues)
+  console.log(age.safeParse(100).error?.issues)
+  console.log(age.safeParse(40))
```

```
[
  {
    code: 'too_small',
    minimum: 20,
    type: 'number',
    inclusive: true,
    exact: false,
    message: '年齢は20歳以上である必要があります',
    path: []
  }
]
[
  {
    code: 'too_big',
    maximum: 80,
    type: 'number',
    inclusive: true,
    exact: false,
    message: '年齢は80歳以下である必要があります',
    path: []
  }
]
{ success: true, data: 40 }
```

良い感じですね。

```min```, ```max``` 関数は```z.string``` でも使用できます。

![cap15_min.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/857657f4-3b26-7e53-e679-79eb78b65fd0.png)

その場合は、```min```, ```max``` で文字数のlengthを判定できます。

```diff_typescript
- const name = z.string({ required_error: "名前は必須です", });
+ const name = z.string({ required_error: "名前は必須です", }).min(1, "1文字以上入力してください");

console.log(name.safeParse(""))
- // → { success: true, data: '' }          // 空文字でも必須チェックはパスできる
+ // → { success: false, error: [Getter] }  // 文字数が0扱いでエラー判定される
```

これでnameフィールドもageフィールドも良い感じですね。

つぎはメールアドレス形式かどうかのチェックを実装してみます！！
よくあるやつ！！

##### Zod よくある形式指定
いろいろ対応してますね

![cap15_mail.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/ff7f4461-5614-7e08-6611-f961db46a706.png)

```diff_typescript
- const email = z.string();
+ const email = z.string().email('メールアドレス形式で入力してください');
+ console.log(email.safeParse("aaa@aaa"))
+ console.log(email.safeParse("aaa@aaa.com"))
+ console.log(email.safeParse("aaa+123@aaa.com"))
```

pnpm tsx src/hello_zod.ts　で実行します

```bash
{ success: false, error: [Getter] }
{ success: true, data: 'aaa@aaa.com' }
{ success: true, data: 'aaa+123@aaa.com' }
```

良い感じです。めっちゃ簡単ですね。
zodにはほかにも便利なvalidationがたくさんあります。

では次はパスワードの形式チェックです。
よくある正規表現チェックですね。

##### Zod 正規表現
zodでは正規表現は ```regex``` 関数で実装できます。

```diff_typescript
- const password = z.string();
+ const password = z.string().regex(/^[a-zA-Z0-9]{8,}$/, 'パスワードは英数字のみを使用し、8文字以上入力してください');
+ console.log(password.safeParse("password"))
+ console.log(password.safeParse("pass1234"))
+ console.log(password.safeParse("pass123"))
+ console.log(password.safeParse("pass!word123"))
```

　↓↓↓
 
```bash
{ success: true, data: 'password' }
{ success: true, data: 'pass1234' }
{ success: false, error: [Getter] }
{ success: false, error: [Getter] }
```

良い感じですね。
最後に確認用パスワードのバリデーションを行います。

##### Zod 相関validation
相関validationは、Userにコードを追加して実装します。

```typescript
const user = z.object({
    name,
    age,
    email,
    password,
    confirmPassword,
});
```

zodには、データを受け取って、カスタムなチェックを行う```refine```, ```superRefine``` という関数が存在します。
この関数を使用してみます。

```diff_typescript
 const user = z.object({
     name,
     age,
     email,
     password,
     confirmPassword,
- });
+ }).refine((data) => data.password === data.confirmPassword, {
+    message: '入力されたパスワードと確認用パスワードが異なります。'
+ });
```

```refine``` 関数は第一引数にカスタムvalidationロジックを実装した関数をとり、
第二引数にエラーメッセージなどの情報を持つオブジェクトをとります。

今回は、dataで受け取った値に存在するdata.password と data.confirmPassword を比較しています。
テストしてみます。

```typescript
console.log(user.safeParse({
    name: 'aaa',
    age: 40,
    email: 'email@qiita.com',
    password: 'password',
    confirmPassword: 'password',
}))
// ↓
// {
//   success: true,
//   data: {
//     name: 'aaa',
//     age: 40,
//     email: 'email@qiita.com',
//     password: 'password',
//     confirmPassword: 'password'
//   }
// }
```

```typescript
console.log(user.safeParse({
    name: 'aaa',
    age: 40,
    email: 'email@qiita.com',
    password: 'password',
    confirmPassword: 'pa$$word',
}).error?.issues)
// ↓
// [ { code: 'custom', message: '入力されたパスワードと確認用パスワードが異なります。', path: [] } ]
```

良い感じですね。refineでなんでもやれそうです！！

ただ、path：[] が見えると思います。
pathとは、zodのどの項目のエラーとするか？を指定する場所です。
やろうと思えば、このエラーをnameのエラーにできたりします。

このエラーは確認用パスワードのエラーとしたいので、pathにはconfirmPasswordを指定します。

```diff_typescript
 const user = z.object({
     name,
     age,
     email,
     password,
     confirmPassword,
  }).refine((data) => data.password === data.confirmPassword, {
+   path: ['confirmPassword'],
    message: '入力されたパスワードと確認用パスワードが異なります。'
  });
```

これで完成です！！

```
名前：必須。
年齢：必須。20歳以上。80歳以下。
メールアドレス：必須。メアド形式必須。
パスワード：必須。8文字以上で英数字のみで構成されていること。
確認用パスワード：必須。パスワードと等しいこと。
```

これで、上記Formを作成する前準備としてzodのオブジェクトを定義できました。
Form側で使用したいので、userと型情報をexportしておきます。

zodオブジェクトのtypeへの変換は、```z.infer<typeof XXXX>``` を使用します。

![cap17.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/58629fb5-58c7-34db-b28a-81874876aad8.png)

これで下準備は完了しました。
最終的なコードを乗せておきます。

```src/hello_zod.ts
import { z } from "zod";

// 名前フィールド
const name = z.string({ required_error: "名前は必須です", }).min(1, "1文字以上入力してください");

// 年齢フィールド（20-80歳）
const age = z.number().min(20, '年齢は20歳以上である必要があります').max(80, '年齢は80歳以下である必要があります');

// メールアドレスフィールド
const password = z.string().regex(/^[a-zA-Z0-9]{8,}$/, 'パスワードは英数字のみを使用し、8文字以上入力してください');

// パスワードフィールド（8文字以上、英数字のみ）
const password = z.string().regex(/^[a-zA-Z0-9]{8,}$/);

// 確認用パスワードフィールド
import { z } from "zod";

// 名前フィールド
const name = z.string({ required_error: "名前は必須です", }).min(1, "1文字以上入力してください");

// 年齢フィールド（20-80歳）
const age = z.number().min(20, '年齢は20歳以上である必要があります').max(80, '年齢は80歳以下である必要があります');

// メールアドレスフィールド
const email = z.string().email('メールアドレス形式で入力してください');

// パスワードフィールド（8文字以上、英数字のみ）
const password = z.string().regex(/^[a-zA-Z0-9]{8,}$/, 'パスワードは英数字のみを使用し、8文字以上入力してください');

// 確認用パスワードフィールド
const confirmPassword = z.string();

export const user = z.object({
    name,
    age,
    email,
    password,
    confirmPassword,
}).refine((data) => data.password === data.confirmPassword, {
    path: ['confirmPassword'],
    message: '入力されたパスワードと確認用パスワードが異なります。'
});

// zodスキーマオブジェクトを型情報に変換してexport
export type User = z.infer<typeof user>;
```

あとはこの型をReact Hook Formで使用してあげるだけです！！


## React Hook Form + Zod で簡単フォーム作成＆バリデーション
React Hook Formには、```@hookform/resolvers``` という関連ライブラリが存在します。
zodやその他のvalidationライブラリとReact Hook Formをコラボさせるためのライブラリです。
今回はこのライブラリを使用し、React Hook Form と Zodを連携させます。

### Zod resolvers ライブラリ用意
ライブラリを追加します。
```bash
pnpm add @hookform/resolvers
```

```bash
Packages: +1
+
Progress: resolved 231, reused 193, downloaded 0, added 1, done

dependencies:
+ @hookform/resolvers 3.9.0

Done in 1.9s
```

良い感じです。

公式のドキュメントを確認します。

https://github.com/react-hook-form/resolvers?tab=readme-ov-file#zod

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

const schema = z.object({
  name: z.string().min(1, { message: 'Required' }),
  age: z.number().min(10),
});

const App = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: zodResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('name')} />
      {errors.name?.message && <p>{errors.name?.message}</p>}
      <input type="number" {...register('age', { valueAsNumber: true })} />
      {errors.age?.message && <p>{errors.age?.message}</p>}
      <input type="submit" />
    </form>
  );
};
```

ふむふむ。

```typescript
  } = useForm({
    resolver: zodResolver(schema),
  });
```

なんとこれだけで良いらしいです。すげー

```typescript
{errors.age?.message && <p>{errors.age?.message}</p>}
```

エラーメッセージはこんな感じでアクセスするようです。
ではこのソースを改造する形で、React Hook Form + Zod でのフォームを実装してみます！！

### 実装

まずは上記ドキュメントからコピペしたコードを少し変更しました。
先ほどexportしたUserの型やzodスキーマをimportし、使用します。

```diff_tsx
  import { useForm } from 'react-hook-form';
  import { zodResolver } from '@hookform/resolvers/zod';
- import * as z from 'zod';       // インポートされたスキーマを使用するため削除
-  
- const schema = z.object({
-   name: z.string().min(1, { message: 'Required' }),
-   age: z.number().min(10),
- });

+ import { User, user } from '../hello_zod'

- const App = () => {
+ export default function App() {  // とりあえず統一
    const {
      register,
      handleSubmit,
      formState: { errors },
-   } = useForm({
-     resolver: zodResolver(schema),
+   } = useForm<User>({           // 型を指定
+     resolver: zodResolver(user),
    });
  
    return (
-     <form onSubmit={handleSubmit((d) => console.log(d))}>
+     <form onSubmit={handleSubmit((d) => console.log(d), (d) => console.log(d))}> // Invalid時もログを表示する
        <input {...register('name')} />
        {errors.name?.message && <p>{errors.name?.message}</p>}
        <input type="number" {...register('age', { valueAsNumber: true })} />
        {errors.age?.message && <p>{errors.age?.message}</p>}
        <input type="submit" />
      </form>
    );
  };
```

こんな変更を加えました。
変更理由はコメントの通りです。diffしないとこんな感じです。

```src/components/ZodForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

import { User, user } from '../hello_zod'

export default function App() {
    const {
        register,
        handleSubmit,
        formState: { errors },
    } = useForm<User>({
        resolver: zodResolver(user),
    });

    return (
        <form onSubmit={handleSubmit((d) => console.log(d), (d) => console.log(d))}>
            <input {...register('name')} />
            {errors.name?.message && <p>{errors.name?.message}</p>}
            <input type="number" {...register('age', { valueAsNumber: true })} />
            {errors.age?.message && <p>{errors.age?.message}</p>}
            <input type="submit" />
        </form>
    );
};
```


足りてないinputはいろいろあるんですが、とりあえず動かしてみましょ！

```src/App.tsx
import ZodForm from "./components/ZodForm"

function App() {
  return (
    <>
      <ZodForm />
    </>
  )
}

export default App
```

App.tsxで表示されるよう編集し、submitしてみました。

![cap18.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/367afe71-9f3d-0b60-f67a-d01da53b51f0.png)

Zodで設定したエラーが良い感じに出ています！！！
無事React Hook Form + Zod の統合がうまくいきました。
あとはinput項目などをこねこねするだけですね。

雑にこんな感じのtsxを作成しました。

```src/components/ZodForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

import { User, user } from '../hello_zod'

export default function App() {
    const {
        register,
        handleSubmit,
        formState: { errors },
    } = useForm<User>({
        resolver: zodResolver(user),
    });

    return (
        <form onSubmit={handleSubmit((d) => console.log(d), (d) => console.log(d))}>
            <input {...register('name')} />
            {errors.name?.message && <p>{errors.name?.message}</p>}
            <input type="number" {...register('age', { valueAsNumber: true })} />
            {errors.age?.message && <p>{errors.age?.message}</p>}
            <input {...register('email')} />
            {errors.email?.message && <p>{errors.email?.message}</p>}
            <input {...register('password')} />
            {errors.password?.message && <p>{errors.password?.message}</p>}
            <input {...register('confirmPassword')} />
            {errors.confirmPassword?.message && <p>{errors.confirmPassword?.message}</p>}
            <input type="submit" />
        </form>
    );
};
```

テストしましょう！！！

![cap19.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/a26ba7af-aa60-51b5-c02c-7c78f0396d74.png)

めっちゃいい！！！

この後はスタイリングや、inputタグやエラーメッセージのコンポーネント化をしていけばいい感じのフォームの実装が完了しますね～

# 終わりに
今回はReact Hook Form + Zod でフォーム、validationの実装をしてみました。

記事用に書いたコードは簡単に検証するためのものですが、他にもいろいろ試せて楽しかったです。

![cap1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/43b3f390-ea8c-b535-3e6d-c77003b22678.png)

興味のある方はリポジトリ覗いてみてください！

https://github.com/suiwave/zod-hello-world