Zodでフォームにバリデーションを追加する
こんにちは!
先日娘と一緒にメザスタで念願のUBをコンプしたwharaguchiです!🪼🎃🪨
今回は、Zod + React Hook Formについての記事になります。
対象読者
- Zodを軽く触ってみたい方
- Zodでどんなことができるか知りたい方
Zod is 何
公式には以下のように書かれています。(DeepLによる訳)
ZodはTypeScriptファーストのスキーマ宣言・検証ライブラリだ。
Zodは可能な限り開発者に優しく設計されている。ゴールは重複した型宣言をなくすことだ。
Zodでは、バリデータを一度宣言すれば、Zodが自動的に静的なTypeScriptの型を推論してくれる。
単純な型を複雑なデータ構造に合成するのも簡単だ。
他にも、依存関係がゼロだったり、Node.jsと全てのモダンブラウザで動作し、プレーンなJavaScriptでも動作する(TypeScriptを使う必要はない)といった点も特徴として挙げられています。
やること
前回書いた記事のバリデーション処理に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も用意しました。
解説
主な変更点は、タイトルの通りZodを使用したバリデーションルールの定義とスキーマの定義、スキーマから型情報を生成した点になります。
具体的に見ていきます。
1. スキーマの作成
今回はtypes.tsファイルを作成し、まずスキーマを定義しました。
+ import { z } from "zod";
+ export const inputs = z.object({
+ firstName: z.string(),
+ lastName: z.string(),
+ comment: z.string(),
+ submit: z.any()
+ });
スキーマ定義については、以下のドキュメントにまとまっていますので、まずはこちらのドキュメントに目を通すといいと思います。
2. スキーマから型を生成
z.infer
を使用することでスキーマから型情報を生成することができます。
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
- type Inputs = {
- firstName: string;
- lastName: string;
- comment: string;
- submit: any;
- };
const {
register,
handleSubmit,
watch,
formState: { errors },
- } = useForm<Inputs>();
after
+ import { InputsType } from "./types";
const {
register,
handleSubmit,
watch,
formState: { errors }
+ } = useForm<InputsType>();
3. Resolverの定義
useFormの引数にresolverを定義することで、バリデーションルールを定義することができます。
今回はZodを使用するので、zodResolver
というライブラリでバリデーションルールを定義しました。
zodResolver
の引数に1で定義したスキーマを渡すことで、バリデーションルールを定義することができます。
+ 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文字以下) |
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でバリデーションを追加する方法になります。
最終的なコードは以下です。
最終的なコード
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;
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にするメリットがあると感じました。
今回は以上となります。
最後まで読んでいただきありがとうございました!
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion