😨

Zodでフォームにバリデーションを追加する

2023/10/30に公開

こんにちは!
先日娘と一緒にメザスタで念願のUBをコンプしたwharaguchiです!🪼🎃🪨

今回は、Zod + React Hook Formについての記事になります。

対象読者

  • Zodを軽く触ってみたい方
  • Zodでどんなことができるか知りたい方

Zod is 何

公式には以下のように書かれています。(DeepLによる訳)

ZodはTypeScriptファーストのスキーマ宣言・検証ライブラリだ。
Zodは可能な限り開発者に優しく設計されている。ゴールは重複した型宣言をなくすことだ。
Zodでは、バリデータを一度宣言すれば、Zodが自動的に静的なTypeScriptの型を推論してくれる。
単純な型を複雑なデータ構造に合成するのも簡単だ。

他にも、依存関係がゼロだったり、Node.jsと全てのモダンブラウザで動作し、プレーンなJavaScriptでも動作する(TypeScriptを使う必要はない)といった点も特徴として挙げられています。

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

やること

前回書いた記事のバリデーション処理にZodを使用してみました。

フォームの仕様は前回と同様以下になります。

項目 必須/任意 その他
必須 -
必須 -
コメント 必須 文字数制限あり(10文字以上、20文字以下)

実装

元となるコード

前回の記事でReact Hook Formを使用したサンプルとして作成したフォームを使用したいと思います。

import { FC } from "react";
import "./styles.css";

import { useForm, SubmitHandler } from "react-hook-form";

type Inputs = {
  firstName: string;
  lastName: string;
  comment: string;
  submit: any;
};

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

  // watch
  const lastName = watch("lastName");

  return (
    <div className="wrapper">
      <h1>React Form</h1>
      <section className="section">
        <h2>useState Form</h2>
        <p>React Hook Formを使用してformを作成した例です。</p>
        <form onSubmit={handleSubmit(onSubmit)}>
          <div className="form-item">
            <label>
              <span className="label required">必須</span>
              <span></span>
              <input
                type="text"
                {...register("lastName", {
                  required: "姓を入力してください",
                })}
              />
            </label>
            {errors.lastName?.message && (
              <p className="error-message">{errors.lastName?.message}</p>
            )}
          </div>
          <div className="form-item">
            <label>
              <span className="label required">必須</span>
              <span></span>
              <input
                type="text"
                {...register("firstName", {
                  required: "名を入力してください",
                })}
              />
            </label>
            {errors.firstName?.message && (
              <p className="error-message">{errors.firstName?.message}</p>
            )}
          </div>
          <div className="form-item">
            <label>
              <span className="label required">必須</span>
              <span>コメント</span>
              <textarea
                {...register("comment", {
                  required: true,
                  minLength: {
                    value: 10,
                    message: "10文字以上で入力してください",
                  },
                  maxLength: {
                    value: 20,
                    message: "20文字以下で入力してください",
                  },
                })}
              />
            </label>
            {errors.comment?.message && (
              <p className="error-message">{errors.comment.message}</p>
            )}
          </div>
          <div className="submit-button">
            <input type="submit" />
          </div>
        </form>
      </section>
      <p>姓: {lastName}</p>
    </div>
  );
};

完成コード

以下が完成したコードになります。
CodeSandboxも用意しました。
https://codesandbox.io/s/react-hook-form-zod-w22trj?file=/src/App.tsx

解説

主な変更点は、タイトルの通りZodを使用したバリデーションルールの定義とスキーマの定義、スキーマから型情報を生成した点になります。

具体的に見ていきます。

1. スキーマの作成

今回はtypes.tsファイルを作成し、まずスキーマを定義しました。

types.ts
+ import { z } from "zod";
+ export const inputs = z.object({
+   firstName: z.string(),
+   lastName: z.string(),
+   comment: z.string(),
+   submit: z.any()
+ });

スキーマ定義については、以下のドキュメントにまとまっていますので、まずはこちらのドキュメントに目を通すといいと思います。
https://github.com/colinhacks/zod#basic-usage

2. スキーマから型を生成

z.inferを使用することでスキーマから型情報を生成することができます。

types.ts
 import { z } from "zod";
 export const inputs = z.object({
   firstName: z.string(),
   lastName: z.string(),
   comment: z.string(),
   submit: z.any()
 });
export type InputsType = z.infer<typeof inputs>;
// type InputsType = {
//   firstName: string;
//   lastName: string;
//   comment: string;
//   submit?: any;
// }

元のコードでApp.tsx内で定義していたInputsを削除し、新たに定義したInputsTypeを適用します。

before
App.tsx
- type Inputs = {
-   firstName: string;
-   lastName: string;
-   comment: string;
-   submit: any;
- };

const {
  register,
  handleSubmit,
  watch,
  formState: { errors },
- } = useForm<Inputs>();
after
App.tsx
+ import { InputsType } from "./types";
const {
  register,
  handleSubmit,
  watch,
  formState: { errors }
+ } = useForm<InputsType>();

3. Resolverの定義

useFormの引数にresolverを定義することで、バリデーションルールを定義することができます。
今回はZodを使用するので、zodResolverというライブラリでバリデーションルールを定義しました。

https://github.com/react-hook-form/resolvers#zod

zodResolverの引数に1で定義したスキーマを渡すことで、バリデーションルールを定義することができます。

App.tsx
+ import { InputsType, inputs } from "./types";
+ import { zodResolver } from "@hookform/resolvers/zod";
const {
  register,
  handleSubmit,
  watch,
  formState: { errors }
 } = useForm<InputsType>({
+  resolver: zodResolver(inputs)
  });

4. バリデーションルールの作成

最後にバリデーションルールをtypes.tsに書いていきます。
今回のバリデーションルールは以下になっています。

項目 必須/任意 その他
必須 -
必須 -
コメント 必須 文字数制限あり(10文字以上、20文字以下)
types.ts
import { z } from "zod";
export const inputs = z.object({
+   firstName: z.string().min(1, {
+     message: "必須項目です。"
+   }),
+   lastName: z.string().min(1, {
+     message: "必須項目です。"
+   }),
+   comment: z
+     .string()
+     .min(10, {
+       message: "10文字以上で入力してください。"
+     })
+     .max(20, {
+       message: "20文字以下で入力してください。"
+     }),
   submit: z.any()
});
export type InputsType = z.infer<typeof inputs>;

各項目が必須項目になっているため、全ての定義に対しminメソッドを使用します。
第一引数に最低入力文字数を入れることができ、条件に達していない場合のエラー文は、messageで定義することができます。

なお、messageを定義しないと以下のように英語でエラーが表示されてしまうため、指定のエラー文がある場合にはここで定義するようにしましょう。

以上がZodでバリデーションを追加する方法になります。
最終的なコードは以下です。

最終的なコード

App.tsx
import { FC } from "react";
import "./styles.css";
import { InputsType, inputs } from "./types";
import { zodResolver } from "@hookform/resolvers/zod";

import {
  useForm,
  SubmitHandler
} from "react-hook-form";

const App: FC = () => {
  const {
    register,
    handleSubmit,
    watch,
    formState: { errors }
  } = useForm<InputsType>({
    resolver: zodResolver(inputs)
  });
  const onSubmit: SubmitHandler<InputsType> = (
    data
  ) => console.log(data);

  console.log("errors", errors);

  // watch
  const lastName = watch("lastName");

  return (
    <div className="wrapper">
      <h1>React Form</h1>
      <section className="section">
        <h2>React Hook Form + Zod</h2>
        <p>
          React Hook Form と
          Zodを使用してformを作成した例です。
        </p>
        <form onSubmit={handleSubmit(onSubmit)}>
          <div className="form-item">
            <label>
              <span className="label required">
                必須
              </span>
              <span></span>
              <input
                type="text"
                {...register("lastName")}
              />
            </label>
            {errors.lastName?.message && (
              <p className="error-message">
                {errors.lastName?.message}
              </p>
            )}
          </div>
          <div className="form-item">
            <label>
              <span className="label required">
                必須
              </span>
              <span></span>
              <input
                type="text"
                {...register("firstName")}
              />
            </label>
            {errors.firstName?.message && (
              <p className="error-message">
                {errors.firstName?.message}
              </p>
            )}
          </div>
          <div className="form-item">
            <label>
              <span className="label required">
                必須
              </span>
              <span>コメント</span>
              <textarea
                {...register("comment")}
              />
            </label>
            {errors.comment?.message && (
              <p className="error-message">
                {errors.comment.message}
              </p>
            )}
          </div>
          <div className="submit-button">
            <input type="submit" />
          </div>
        </form>
      </section>
      <p>姓: {lastName}</p>
    </div>
  );
};
export default App;
types.ts
import { z } from "zod";

export const inputs = z.object({
  firstName: z.string().min(1, {
    message: "必須項目です。"
  }),
  lastName: z.string().min(1, {
    message: "必須項目です。"
  }),
  comment: z
    .string()
    .min(10, {
      message: "10文字以上で入力してください。"
    })
    .max(20, {
      message: "20文字以下で入力してください。"
    }),
  submit: z.any()
});
export type InputsType = z.infer<typeof inputs>;

最後に

今回は基本的な部分に触れるのみでしたが、いかがでしたでしょうか?

個人的にはZodを使用することで、ロジックからバリデーションルールを分離できたことが大きいと感じました。
より項目数が多いフォームなどでは、分離されていないと可読性が低くなり、開発につらみが出てくると思いますので、そういった意味でもZodにするメリットがあると感じました。

今回は以上となります。
最後まで読んでいただきありがとうございました!

GitHubで編集を提案
スペースマーケット Engineer Blog

Discussion