react-hook-form が Valibot に対応、Zod比較でバンドルサイズが92%削減
はじめに
8 月 6 日に @hookform/resolvers
が Valibot を対応したので、react-hook-form
で Valibot を利用したバリデーションができるようになりました。Zodと比較し、問い合わせフォームの実装で、バンドルサイズが92%削減することを確認しました。
誤解を与えたと思いますが、問い合わせフォーム全体のバンドルサイズが、92%削減したわけではありません。Zod から読み込んだパッケージのサイズが 12.8k に対して、Valibot は 1k のため、型検証のパッケージに関して、バンドルサイズが 92%削減となります。小さいは正義!
本記事では以下を記述しています。
- Valibot とについて
- Zod との比較
- Valibot を利用した問い合わせフォームの実装
本記事の実装リポジトリ(Valibot を利用したフォームの実装)はこちらです。
別記事の実装リポジトリ(Zod を利用したフォームの実装)はこちら、記事はこちらです。
Valibot とは❓
Valibot とは一言でいうと、Zod と同じようなバリデーションライブラリです。
基本的に Zod と同じことができます。
サイト名 | URL |
---|---|
Valibot 公式サイト | https://valibot.dev/ |
Valibot Github のリポジトリ | https://github.com/fabian-hiller/valibot |
Zod 公式サイト | https://zod.dev/ |
Zod Github のリポジトリ | https://github.com/colinhacks/zod |
Zodとの比較
公式サイトで、Valibot は、Zod と比較して、バンドルサイズが最大98%削減できると記述されています。今回作成した問い合わせフォームでも、92.2%の削減を確認できました。
VSCode 上で Zod で作成した Schema ファイルのサイズは gzipped 圧縮で12.8kです(Zod を利用した Schema はこちらを参照ください)。
ZodのSchema実装
import { z } from "zod";
const email: z.ZodString = z
.string({ required_error: "入力が必須の項目です" })
.min(1, { message: "入力が必須の項目です" })
.max(255, { message: "255文字以内で入力してください" })
.email({ message: "メールアドレスの形式で入力してください" });
const telephone: z.ZodString = z
.string({ required_error: "入力が必須の項目です" })
.min(10, { message: "電話番号を入力してください" })
.max(14, { message: "入力値が長すぎます" });
const givenName: z.ZodString = z
.string({ required_error: "入力が必須の項目です" })
.min(1, { message: "入力が必須の項目です" })
.max(20, { message: "入力値が長すぎます" });
const lastName: z.ZodString = z
.string({ required_error: "入力が必須の項目です" })
.min(1, { message: "入力が必須の項目です" })
.max(20, { message: "入力値が長すぎます" });
const organizationName: z.ZodString = z
.string({ required_error: "入力が必須の項目です" })
.min(1, { message: "入力が必須の項目です" })
.max(50, { message: "入力値が長すぎます" });
const message: z.ZodString = z
.string({ required_error: "入力が必須の項目です" })
.min(1, { message: "入力が必須の項目です" })
.max(4098, { message: "入力値が長すぎます" });
const agree: z.ZodLiteral<string> = z.literal("true", {
errorMap: () => ({ message: "同意が必須です" }),
});
export const ContactSchema = z.object({
email: email,
telephone: telephone,
givenName: givenName,
lastName: lastName,
organizationName: organizationName,
message: message,
agree: agree,
});
export type ContactType = z.infer<typeof ContactSchema>;
Valibot は1kでした(こちらは本記事で実装します)。
ValibotのSchema実装
import {
LiteralSchema,
Output,
StringSchema,
email,
literal,
maxLength,
minLength,
object,
string,
} from "valibot";
const emailSchema: StringSchema<string> = string([
minLength(1, "入力が必須の項目です"),
maxLength(255, "255文字以内で入力してください"),
email("メールアドレスの形式で入力してください"),
]);
const telephoneSchema: StringSchema<string> = string([
minLength(1, "電話番号を入力してください"),
maxLength(11, "入力値が長すぎます"),
]);
const givenNameSchema: StringSchema<string> = string([
minLength(1, "入力が必須の項目です"),
maxLength(20, "入力値が長すぎます"),
]);
const lastNameSchema: StringSchema<string> = string([
minLength(1, "入力が必須の項目です"),
maxLength(20, "入力値が長すぎます"),
]);
const organizationNameSchema: StringSchema<string> = string([
minLength(1, "入力が必須の項目です"),
maxLength(50, "入力値が長すぎます"),
]);
const messageSchema: StringSchema<string> = string([
minLength(1, "入力が必須の項目です"),
maxLength(4098, "入力値が長すぎます"),
]);
const agreeSchema: LiteralSchema<"true", "true"> = literal(
"true",
"同意が必要です"
);
export const ContactSchema = object({
email: emailSchema,
telephone: telephoneSchema,
givenName: givenNameSchema,
lastName: lastNameSchema,
organizationName: organizationNameSchema,
message: messageSchema,
agree: agreeSchema,
});
export type ContactType = Output<typeof ContactSchema>;
Valibotを利用した問い合わせフォームの実装
以降、Valibot を利用し問い合わせフォームを作成していきます。
- react-hook-form で state管理 します。
- Valibot でバリデーションをします。
- TailwindCSS で見た目を作ります。
事前環境の構築
作業するための Next.js プロジェクトを作成します。長いので、折り畳んでおきます。
新規プロジェクト作成と初期環境構築の手順詳細
プロジェクトを作成
create next-app@latest
でプロジェクトを作成します。
$ pnpm create next-app@latest nextjs-valibot-sample --typescript --eslint --import-alias "@/*" --src-dir --use-pnpm --tailwind --app
$ cd nextjs-valibot-sample
Peer Dependenciesの警告を解消
Peer dependenciesの警告が出ている場合は、pnpm install
を実行し、警告を解消します。
WARN Issues with peer dependencies found
.
├─┬ autoprefixer 10.0.1
│ └── ✕ unmet peer postcss@^8.1.0: found 8.0.0
├─┬ tailwindcss 3.3.0
│ ├── ✕ unmet peer postcss@^8.0.9: found 8.0.0
│ ├─┬ postcss-js 4.0.1
│ │ └── ✕ unmet peer postcss@^8.4.21: found 8.0.0
│ ├─┬ postcss-load-config 3.1.4
│ │ └── ✕ unmet peer postcss@>=8.0.9: found 8.0.0
│ └─┬ postcss-nested 6.0.0
│ └── ✕ unmet peer postcss@^8.2.14: found 8.0.0
└─┬ next 14.0.4
├── ✕ unmet peer react@^18.2.0: found 18.0.0
└── ✕ unmet peer react-dom@^18.2.0: found 18.0.0
以下を実行することで警告が解消されます。
$ pnpm i -D postcss@latest react@^18.2.0 react-dom@^18.2.0
不要な設定を削除し、プロジェクトを初期化します。
styles
CSSなどを管理するstylesディレクトリを作成します。globals.css
を移動します。
$ mkdir -p src/styles
$ mv src/app/globals.css src/styles/globals.css
globals.css
の内容を以下のように上書きします。
@tailwind base;
@tailwind components;
@tailwind utilities;
初期ページ
app/page.tsx
を上書きします。
import { type FC } from "react";
const Home: FC = () => {
return (
<div className="">
<div className="text-lg font-bold">Home</div>
<div>
<span className="text-blue-500">Hello</span>
<span className="text-red-500">World</span>
</div>
</div>
);
};
export default Home;
レイアウト
app/layout.tsx
を上書きします。
import "@/styles/globals.css";
import { type FC } from "react";
type RootLayoutProps = {
children: React.ReactNode;
};
export const metadata = {
title: "Sample",
description: "Generated by create next app",
};
const RootLayout: FC<RootLayoutProps> = (props) => {
return (
<html lang="ja">
<body className="">{props.children}</body>
</html>
);
};
export default RootLayout;
TailwindCSSの設定
TailwindCSSの設定(tailwind.config.ts
)を上書きします。
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
plugins: [],
}
export default config
TypeScriptの設定
TypeScriptの設定(tsconfig.json
)を上書きします。
{
"compilerOptions": {
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"checkJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"noUncheckedIndexedAccess": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"plugins": [
{
"name": "next"
}
],
},
"include": [
".eslintrc.cjs",
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.cjs",
"**/*.mjs",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
}
動作確認
ローカルで動作確認します。
$ pnpm run dev
コミットして作業結果を保存しておきます。
$ git add .
$ git commit -m "feat:新規にプロジェクトを作成し, 作業環境を構築"
Valibotをインストール
Valibot のパッケージをインストールします。
$ pnpm add valibot
react-hook-form
, @hookform/resolvers
関連のパッケージをインストールします。
$ pnpm add react-hook-form
$ pnpm add @hookform/resolvers
コミットします。
$ pnpm build
$ git add .
$ git commit -m "パッケージをインストール"
Schemaを作成
Valibot で利用する schema を作成します。
ファイル名 | 説明 |
---|---|
contact.ts | 問い合わせフォームの入力値をバリデーションする schema |
contactSimple.ts | 問い合わせフォームの入力値をバリデーションする schema (簡易版) |
$ mkdir src/schema
$ touch src/schema/contact.ts
$ touch src/schema/contact-simple.ts
import {
LiteralSchema,
Output,
StringSchema,
email,
literal,
maxLength,
minLength,
object,
string,
} from "valibot";
const emailSchema: StringSchema<string> = string([
minLength(1, "入力が必須の項目です"),
maxLength(255, "255文字以内で入力してください"),
email("メールアドレスの形式で入力してください"),
]);
const telephoneSchema: StringSchema<string> = string([
minLength(1, "電話番号を入力してください"),
maxLength(11, "入力値が長すぎます"),
]);
const givenNameSchema: StringSchema<string> = string([
minLength(1, "入力が必須の項目です"),
maxLength(20, "入力値が長すぎます"),
]);
const lastNameSchema: StringSchema<string> = string([
minLength(1, "入力が必須の項目です"),
maxLength(20, "入力値が長すぎます"),
]);
const organizationNameSchema: StringSchema<string> = string([
minLength(1, "入力が必須の項目です"),
maxLength(50, "入力値が長すぎます"),
]);
const messageSchema: StringSchema<string> = string([
minLength(1, "入力が必須の項目です"),
maxLength(4098, "入力値が長すぎます"),
]);
const agreeSchema: LiteralSchema<"true", "true"> = literal(
"true",
"同意が必要です"
);
export const ContactSchema = object({
email: emailSchema,
telephone: telephoneSchema,
givenName: givenNameSchema,
lastName: lastNameSchema,
organizationName: organizationNameSchema,
message: messageSchema,
agree: agreeSchema,
});
export type ContactType = Output<typeof ContactSchema>;
import {
Output,
email,
literal,
maxLength,
minLength,
object,
string,
} from "valibot";
export const ContactSchema = object({
email: string([
minLength(1, "入力が必須の項目です"),
maxLength(255, "255文字以内で入力してください"),
email("メールアドレスの形式で入力してください"),
]),
telephone: string([
minLength(1, "電話番号を入力してください"),
maxLength(11, "入力値が長すぎます"),
]),
givenName: string([
minLength(1, "入力が必須の項目です"),
maxLength(20, "入力値が長すぎます"),
]),
lastName: string([
minLength(1, "入力が必須の項目です"),
maxLength(20, "入力値が長すぎます"),
]),
organizationName: string([
minLength(1, "入力が必須の項目です"),
maxLength(50, "入力値が長すぎます"),
]),
message: string([
minLength(1, "入力が必須の項目です"),
maxLength(4098, "入力値が長すぎます"),
]),
agree: literal("true", "同意が必要です"),
});
export type ContactType = Output<typeof ContactSchema>;
コミットします。
$ git add .
$ git commit -m "Schemaを作成"
フォームのコンポーネントを作成
問い合わせフォームのコンポーネントを作成します。
- react-hook-form で state 管理
- Valibot でバリデーション
- TailwindCSS で見た目を作成
以下が仕様です。
- 入力値のバリデーションを Valibot で実行しています。
- 入力値が正しくないときは、エラーメッセージが画面に表示されます。
- 入力値が正しくないときは、送信ボタンをクリックできません。
- 送信ボタンをクリックすると、クライアントのコンソールに入力値が表示されます。
$ mkdir src/components
$ touch src/components/contact-form.tsx
"use client";
import { FC } from "react";
import { ContactSchema, ContactType } from "@/schema/contact";
import { valibotResolver } from "@hookform/resolvers/valibot";
import { SubmitHandler, useForm } from "react-hook-form";
interface ContactFormProps {}
const ContactForm: FC<ContactFormProps> = ({}) => {
const handleOnSubmit: SubmitHandler<ContactType> = (data) => {
console.log(data);
};
const {
register,
handleSubmit,
formState: { errors: formatError, isValid, isSubmitting },
} = useForm<ContactType>({
mode: "onBlur",
resolver: valibotResolver(ContactSchema),
});
return (
<form
method="post"
onSubmit={(event) => {
void handleSubmit(handleOnSubmit)(event);
}}
className="flex flex-col space-y-10"
>
<label className="flex flex-col space-y-1">
<div className="text-sm font-bold mb-1">メールアドレス</div>
<input
type="text"
{...register("email")}
className="text-gray-800 mt-4 rounded-md border py-2 px-3"
placeholder="例)mail@example.com"
/>
{formatError.email && (
<div className="text-red-500 pl-1 pt-1 text-xs">
{formatError.email.message}
</div>
)}
</label>
<label className="flex flex-col space-y-1">
<div className="text-sm font-bold mb-1">電話番号</div>
<input
type="text"
{...register("telephone")}
className="text-gray-800 mt-4 rounded-md border py-2 px-3"
placeholder="例)09012345678"
/>
{formatError.telephone && (
<div className="text-red-500 pl-1 pt-1 text-xs">
{formatError.telephone.message}
</div>
)}
</label>
<label className="flex flex-col space-y-1">
<div className="text-sm font-bold mb-1">お名前</div>
<input
type="text"
{...register("lastName")}
className="text-gray-800 mt-4 rounded-md border py-2 px-3"
placeholder="例)山田"
/>
{formatError.lastName && (
<div className="text-red-500 pl-1 pt-1 text-xs">
{formatError.lastName.message}
</div>
)}
<input
type="text"
{...register("givenName")}
className="text-gray-800 mt-4 rounded-md border py-2 px-3"
placeholder="例)太郎"
/>
{formatError.givenName && (
<div className="text-red-500 pl-1 pt-1 text-xs">
{formatError.givenName.message}
</div>
)}
</label>
<label className="flex flex-col space-y-1">
<div className="text-sm font-bold mb-1">企業名</div>
<input
type="text"
{...register("organizationName")}
className="text-gray-800 mt-4 rounded-md border py-2 px-3"
placeholder="例)株式会社◯✕△"
/>
{formatError.organizationName && (
<div className="text-red-500 pl-1 pt-1 text-xs">
{formatError.organizationName.message}
</div>
)}
</label>
<label className="flex flex-col space-y-1">
<div className="text-sm font-bold mb-1">お問い合わせ内容</div>
<textarea
{...register("message")}
className="h-36 border px-2 py-1"
></textarea>
{formatError.message && (
<div className="text-red-500 pl-1 pt-1 text-xs">
{formatError.message.message}
</div>
)}
</label>
<div className="flex flex-col items-center space-y-1">
<div className="flex flex-row items-center space-x-2">
<label className="flex flex-row items-center space-x-2">
<input
type="checkbox"
value="true"
{...register("agree")}
className="h-5 w-5"
/>
<p>個人情報取り扱いに同意する</p>
</label>
</div>
{formatError.agree && (
<div className="text-red-500 pl-1 pt-1 text-center text-xs">
{formatError.agree.message}
</div>
)}
</div>
<button
type="submit"
disabled={!isValid || isSubmitting}
className="bg-slate-800 hover:bg-slate-600 rounded px-4 py-2 text-white disabled:bg-gray-300 md:self-center"
>
送信する
</button>
</form>
);
};
export default ContactForm;
コミットします。
$ git add .
$ git commit -m "フォームコンポーネントを作成"
ページを修正
下記の通り、page.tsx
を上書きします。
import ContactForm from "@/components/contact-form";
export default function Home() {
return (
<main className="container relative mx-auto">
<div className="mx-auto lg:w-[800px] py-10 px-10">
<h1 className="text-2xl font-bold text-center pb-10">お問い合わせ</h1>
<ContactForm />
</div>
</main>
);
}
コミットします。
$ git add .
$ git commit -m "ページにフォームを追加"
動作確認
ローカルサーバで動作確認をします。
$ pnpm run dev
バリデーションが正しく動作していることが確認できます。適切に入力した後、送信ボタンをクリックすると、コンソールに入力値が表示されます。
送信ボタンをクリック後に、クライアントのコンソールに入力値を表示されていることが確認できます。
まとめ
- 問い合わせフォームを作成しました。
- react-hook-form で state管理。
- Valibot で バリデーション。
- TailwindCSS で 見た目 を作成。
- 以下の機能を実装しました。
- 入力値のバリデーションを Valibot で実行。
- 入力値が正しくないときは、エラーメッセージを画面に表示。
- 入力値が正しくないときは、送信ボタンのクリックを不可。
- 送信ボタンをクリックすると、クライアントのコンソールに入力値を表示。
- Zodと比較し、バンドルサイズが92%削減することを確認しました。
以下が作業用のリポジトリです。
Discussion
react-hook-formが92%削減されたかと思ってミスリーディングとも思いましたが、小さくなるのはいいことですよね。
zod→valibotで%的には大きく削減されるけど、他のパッケージもどうせある中でどれほど効果があるのかうーんという気も。
コメントありがとうございます。
ちょっとタイトルとしては、大げさだった気もしますが、Valibotに興味持っていただければと思います。
少しだけタイトル修正しました。まだミスリーディングしてますが、タイトルということでご了承を!(汗)
修正前:react-hook-form が Valibot に対応、バンドルサイズが92%削減
⬇
修正後:react-hook-form が Valibot に対応、Zod比較でバンドルサイズが92%削減
zodのサイズも大きくはないですし、zodはすでにエコシステムがあるので、Valibotに乗り換えるべきかどうかはリスクあるかなとは思います。ただ、小さくなることはいいことですよね。使用感もzod比較して決して悪くないと思います。
Didn't find a global error message customization in valibot's documentation, just like
setLocale
in yup .この記事の書き方は古いので注意してください!!
v0.31.0以上では動きません