1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

ReactでForm実装といえば React Hook Form !!

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

cap2.PNG

validationといえば Zod !!

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

cap1.PNG

もくじ

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

プロジェクト用意

vite + Reactプロジェクト用意

pnpm create vite zod-hello-world --template react-ts

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

cd zod-hello-world

移動して・・・。

pnpm install

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

pnpm run dev

はろーわします。

cap3.PNG

良い感じですね!

viteやpnpmって何?って方は以下の記事もぜひ読んでみてください!(宣伝)

React Hook Form をいろいろさわる

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

React Hook Form ライブラリ用意

pnpm add react-hook-form

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

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をふんわり理解します。

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

コンポーネント準備

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

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

cap5.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

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

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

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

cap7.PNG

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

コードを修正して確認します。

- 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関数が何を返すか確認します。

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

cap8.PNG

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

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

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

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

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")} />

※参考

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

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

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

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

cap9.PNG

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

cap10.PNG

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

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

Zodに入門してみる

Zod ライブラリ用意

addしていきますー

pnpm add zod
Packages: +1
+
Progress: resolved 230, reused 192, downloaded 0, added 1, done

dependencies:
+ zod 3.23.8

Done in 1.5s

おっけーですね。

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

pnpm add -D tsx
Packages: +5
+++++
Progress: resolved 259, reused 198, downloaded 0, added 5, done

devDependencies:
+ tsx 4.19.1

Done in 4.2s

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

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

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

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

やっていきましょう!

Zod 入門

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

ドキュメントより引用。

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

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

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

cap14.PNG

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

    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してみます

pnpm tsx .\src\hello_zod.ts

結果が出ます。

{ success: false, error: [Getter] }

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

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

-   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する引数で実行してみます。

-   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

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

-   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

-  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

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

- 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

- 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 で実行します

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

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

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

Zod 正規表現

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

- 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"))

 ↓↓↓

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

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

Zod 相関validation

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

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

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

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

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

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

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'
//   }
// }
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を指定します。

 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

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

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 ライブラリ用意

ライブラリを追加します。

pnpm add @hookform/resolvers
Packages: +1
+
Progress: resolved 231, reused 193, downloaded 0, added 1, done

dependencies:
+ @hookform/resolvers 3.9.0

Done in 1.9s

良い感じです。

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

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>
  );
};

ふむふむ。

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

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

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

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

実装

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

  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

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

めっちゃいい!!!

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

終わりに

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

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

cap1.PNG

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?