本記事のゴール
Next.jsプロジェクトに、サクッとバリデーション処理を追加したい。でき上がりのサンプルプロジェクトは下記にあります。
作業
今回は友人から zodがいいぞ、という話を聞いていたので迷わず試してみようと思いました。
今回は、
- price という項目に、入力有無と整数値のバリデーションを追加し、
- 入力が失敗するときはエラーメッセージを出す
- 入力が失敗するときは送信ボタンを押せなくしたい。
- これ単体ではフォームバリデーションができないので、一番採用事例の多く
A first-party Zod resolver for React Hook Form.
とzodのドキュメントにも書かれているReact Hook Formを使う。
という条件で進めてみます。zodのドキュメントBasic Usageあたりをヒントに使います。
今回のサンプルプロジェクトはNext.js 13で作られていまして、エントリーポイントはsrc/apps/page.tsx
です。こちらにゴリゴリフロントエンドに相当する処理を作っていきます。
zodをinstall
以下はcleanインストールから作成する時のみの手順です。まず、Next.jsのプロジェクトルートで以下を打ちます。
yarn add zod react-hook-form @hookform/resolvers
最後の@hookform/resolvers
は、react-hook-formでzodを利用するために必ず必要なのでもれなくインストールしてください。
インストールが完了したら、ルートで
yarn run dev
を実行し、開発環境を立ち上げます。
- 対象のComponentでライブラリをインポート。
- バリデーション定義を
zod
で書く。 - フォーム管理を
react-hook-form
に一任する処理を書く。 - Next.jsのjsxで書かれたフォーム処理に、
react-hook-form
のバリデーション処理、エラー表示処理を追加していく。
という流れとなります。
完成したソースコードのサンプルを以下に書きます(前述の1〜4の手順をコメントで追加していきます。)
1.対象のComponentでライブラリをインポート。
"use client";
import { FieldValues, SubmitHandler, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { useState } from 'react';
2.バリデーション定義をzodで書く。
下記は、
- パスワードは8文字以上、20文字以下のみ受け付ける
- パスワードと、パスワード(確認)の一致を確認する
を行なっていますが、要件に合わせて改変してください。
PHP系フレームワーク(バックエンドで私がよく使う)のこういうバリデーション処理って、retype_password
のバリデーション定義で一致もそのまま書けるイメージがありますが、Zodでは
refine()
というメソッドを後付けで追加してバリデーションします。少し癖があるので要注意です。
const schema = z.object({
email: z.string().email({ message: "メールアドレスを正しく入力してください" }),
password: z.string().min(8, "パスワードを8文字以上で入力してください").max(20, "パスワードを20文字以下で入力してください"),
retype_password: z.string().min(8, "パスワードを8文字以上で入力してください").max(20, "パスワードを20文字以下で入力してください")
})
.refine((value) => value.password === value.retype_password, {
message: "パスワードが一致しません",
path: ['retype_password']
})
3.フォーム管理をreact-hook-formに一任する処理を書く。
ここはいわゆるおまじないです。react-hook-formは通常のhtmlに、バリデーション処理を簡便に注入するライブラリでして、そのバリデーションの成否を判定するリソルバは別途設定せねばなりません。Zod以外にも、Yup
やSuperstruct
などさまざまなリソルバが用意されているようです。もし興味があれば、以下のリンクを参照してみてください。
私の方では、公式ドキュメントに載っている一般的なやり方で実装していきます。
const Page = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(schema),
});
const [loading, setLoading] = useState(false) // ロード処理
const onSubmit: SubmitHandler<FieldValues> = (values) => {
setLoading(true)
// こちらで、APIへのポストなどを行う
setLoading(false)
// alertで簡便に出力しているが、本来は、適宜要件に合わせてUI処理する
alert("処理を完了しました。")
}
return (
// ここは4.の章でかく
)
}
4.Next.jsのjsxで書かれたフォーム処理に、react-hook-formのバリデーション処理、エラー表示処理を追加していく。
あとはゴリゴリreact-hook-formの注入処理をjsxに入れていくのみです。(重複するので「完全なコード」の欄で書きます。)
完全なコード
最終的に、Next.jsプロジェクトのエントリーポイントとなるsrc/app/page.tsx
のソースコードは以下となります。先述の通りNext 13プリインのtailwindcssをそのまま使っているので、1ファイルでほぼ処理が完結しています。
"use client";
import { FieldValues, SubmitHandler, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { useState } from 'react';
const schema = z.object({
email: z.string().email({ message: "メールアドレスを正しく入力してください" }),
password: z.string().min(8, "パスワードを8文字以上で入力してください").max(20, "パスワードを20文字以下で入力してください"),
retype_password: z.string().min(8, "パスワードを8文字以上で入力してください").max(20, "パスワードを20文字以下で入力してください")
})
.refine((value) => value.password === value.retype_password, {
message: "パスワードが一致しません",
path: ['retype_password']
})
const Page = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(schema),
});
const [loading, setLoading] = useState(false)
const onSubmit: SubmitHandler<FieldValues> = (values) => {
setLoading(true)
// こちらで、APIへのポストなどを行う
setLoading(false)
// alertで簡便に出力しているが、本来は、適宜要件に合わせてUI処理する
alert("処理を完了しました。")
}
return (
<main className="m-10 p-5 bg-white">
<form onSubmit={handleSubmit(onSubmit)}>
<h2 className="text-xl font-bold leading-loose pt-6">Zodの動作確認を行うサンプルプロジェクトです。</h2>
<div className="pt-6">
<h5 className="font-bold">メールアドレス</h5>
<div className="py-2 w-full flex">
<input {...register('email')} className={"w-full px-6 py-4 rounded-md text-xl" + (errors.email ? " border-red-500 border-2" : " border")} placeholder="メールアドレスを入力" />
</div>
{errors.email && <div className="text-red-500">{errors?.email?.message?.toString()}</div>}
</div>
<div className="pt-6">
<h5 className="font-bold">パスワード</h5>
<div className="py-2 w-full flex">
<input
{...register('password')}
className={"w-full px-6 py-4 rounded-md text-xl" + (errors.password ? " border-red-500 border-2" : " border")}
type="password"
placeholder="パスワードを入力してください"
/>
</div>
{errors.password && <div className="text-red-500">{errors?.password?.message?.toString()}</div>}
</div>
<div className="pt-6">
<h5 className="font-bold">パスワード(確認)</h5>
<div className="py-2 w-full flex">
<input
{...register('retype_password')}
className={"w-full px-6 py-4 rounded-md text-xl" + (errors.retype_password ? " border-red-500 border-2" : " border")}
type="password"
placeholder="パスワード(確認)を入力してください"
/>
</div>
{errors.retype_password && <div className="text-red-500">{errors?.retype_password?.message?.toString()}</div>}
</div>
<div className="w-full pt-12 pb-6">
<button type="submit" className="w-full bg-blue-300 hover:bg-blue-200 text-gray-700 font-semibold px-4 py-4 rounded-md">
送信
</button>
</div>
</form>
</main >
);
};
export default Page
バリデーションの逆引き一覧
以下に、私のほうで実際に使ったさまざまなバリデーションパターンを逆引き形式で、気づき次第追記していきます。
URLのバリデーションをするには?
url: z.string().url({ message: 'URLを入力してください' }),
実際に試したところ、http://
またはhttps://
が入力されているかどうかが条件で、.com
などのドメイン形式となっているかどうかは
判定条件ではないようです。よって、ドメインのバリデーションが必要な場合 は以下のように正規表現を使います。
.regex(/^https?:\/\/(.+?)\.(.+?)/, { message: 'URLを入力してください' }),
相対パスのバリデーションをするには?
./
で始まるURLのバリデーションをするには以下のように正規表現を使います。./hogehoge
や./hogehoge/fugafuga
といった多階層の相対パスも一致します。
path: z.string().regex(/^\.\/(.*)/, { message: 'パスを相対パスの形式で入力してください。' }),
ちなみに絶対パスのバリデーションをしたい時も大きな変更はないです。\.
を取るだけです。
path: z.string().regex(/^\/(.*)/, { message: 'パスを絶対パスの形式で入力してください。' }),
値が数値のセレクトボックスのバリデーションをするには?
以下のようにします。ポイントは、valueAsNumber
をオプションで指定し、数字としてzodで判定するようにすること。
<select name="pref" {...register('pref',{ valueAsNumber: true })}>
<option value={0}>未選択</option>
<option value={1}>北海道</option>
<option value={2}>青森県</option>
// 中略
</option value={47}>沖縄県</option>
</select>
こうすることで、未選択を0として扱い、1以上をバリデーション通過というシンプルな形で判定が可能です。
pref: z.number().min(1, { message: '都道府県を選択してください' }),
日本のハイフン付き電話番号のバリデーションをするには?
日本のハイフン付きの電話番号 の場合ですと、
- 03-1234-5678
- 050 or 070 or 080 or 090-1234-5678
といった電話番号以外にも複雑なパターンがありそうですが、問い合わせなどのケースを考えて、一般的な電話番号を対象に緩めにバリデーションしておきます。
contact_tel: z.string().nullable()
.refine((value) => {
const phoneNumberPattern = /^0\d{2,3}-\d{3,4}-\d{3,4}$/; //
// nullまたは空文字列の場合は検証をスキップ
if (value === null || value === '') {
return true;
}
return phoneNumberPattern.test(value);
}, { message: '正しい電話番号を入力してください' }),
(もう少し厳しいバリデーションルールにニーズがありそうならば、記事化します)
利用規約などチェックボックスのバリデーションをするには?
下記のようなチェックボックスタイプのパーツは、
<div className="flex items-start mb-1">
<div className="flex items-center h-5">
<input id="term" type="checkbox" {...register('term', { required: true })} value="" className={border border-gray-300 w-4 h-4 rounded bg-gray-50"} />
</div>
<label htmlFor="term" className="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300"><Link href="/term" className="text-blue-600 hover:underline dark:text-blue-500">ご利用規約</Link>に同意する</label>
</div>
このように、refine()
を使って判定を行います。
term: z.boolean().refine((value) => value === true, { message: '注意事項・利用規約をお読みいただき同意してください。' }),
まとめ
Next.js + react-hook-form + zodの組み合わせで、一度雛形を作ってしまえばform処理をすごいスピード(Ruby on RailsやLaravelなどの古典的なバック・フロント同梱のフレームワークと比べ)で量産できます。 ただ、DBと連関する処理や認証処理などは別途用意せねばなりませんので、用途によってはという注意書きはつきますが、少なくとも初学者などには非常に向いているソリューションだと思いました。
補足: 本ブログのNext.js関連記事
※ 2023.11.19 追記
ブログを色々書き進めて、React/Next.js関連の記事がちょっと増えてきたので、こちらに併記させていただきます。もしよろしければお読みくださいませ。
以上でございます。
この記事が何かのお役に立てれば幸いです。最後までお読みいただきありがとうございました!