はじめに
フォームのバリデーションをクライアントサイドで実装して、サーバーサイドで実装して、それらを良い感じに繋げて、とやっていくとわりとめんどうだったので React Hook Form と yup または zod の組み合わせで実装してみた。
環境
- TypeScript 4.7.x
- Next.js 12.2.x
- React Hook Form 7.33.x
- yup 0.32.x
- zod 3.17.x
ディレクトリ構成
Next.js なプロジェクトの場合、以下のような感じになった:
├── package.json
├── src
│ ├── pages
│ │ ├── ...
│ │ ├── index.tsx
│ │ ├── yup.tsx
│ │ └── zod.tsx
│ ├── styles
│ │ ├── app.css
│ │ └── form.module.css
│ └── validations
│ ├── yup-locale.ts
│ ├── yup-schema1.ts
│ └── zod-schema1.ts
├── ...
Schema Validation の実装
ここでいう Schema とは Database Table の Column だと思っていい。そしてここで実装するものはあくまでクライアントサイドのバリデーションだということを認識しておく。
pages ディレクトリ以下のコンポーネントに直接書いてもいいわけだが、例えば yup から zod に移行したいなぁってときに楽ができるように validations ディレクトリ以下に詳細を書くことにする。これで差し替えが容易になる。
yup は locale をセットできるのでやっておく。zod はない?
だいたい上記のような書き方に落ち着いた。これでほぼすべて VSCode などで補完が効くし、変なもんが混じってるとエラーで教えてくれるし、ある程度の秩序は保つことができる。
zod で書く場合は以下:
書き方としては yup と非常に似ているが、locale と label がないので若干管理がめんどくさそう。また、 yup より zod のほうが型については厳密さがあり、React Hook Form と組み合わせた場合、ちょっとした工夫が必要になったりする (詳細はここでは省く) 。
フォームの実装
React Hook Form と yup を組み合わせたもの:
React Hook Form と zod を組み合わせたフォーム:
Form validation 用の Schema の定義を外に逃しているので、 yup, zod ともにほぼ同じ書き方になる。ただし number 型とか出てくると React Hook Form + zod を組み合わせた場合に { valueAsNumber: true } みたいなオプションを書かないといけなかったりするので注意が必要。
サーバーサイドバリデーションと組み合わせる
上記でやってきたことはすべてクライアントサイドのバリデーションであるため、サーバーサイドのバリデーションと良い感じに繋げるための実装が必要になってくる。
以下のようになった:
HTTP Client によって書き方が変わるし、認証情報なども必要であったり、Response の形式や構造も様々だが、API サーバーにアクセスしてサーバーサイドのバリデーションに失敗した場合に返ってくる内容を useForm の setError に入れてやる、という流れ。これでクライアントサイド、サーバーサイド両方のバリデーション失敗時のメッセージを良い感じに表示できるようになる。
まとめ
今回紹介した内容はあくまで基本であって、実際プロジェクトに導入する場合はより工夫が必要になってくる。そのために React Hook Form では Advanced なドキュメントが設けられているので是非読んでおきたい。
備考
ちなみに、この記事を書くにあたって Blitz を少し触ってみたのだが (zod を採用しているため)、バージョン 0.34.x 現在では React Final Form を推奨していて (React Hook Form も使える)、CLI でモデルなどのファイルを自動生成した場合の内容がわりと興味深かったので、気になる人は触ってみるのも良いかもしれない。
また、クライアントサイド、サーバーサイドのバリデーションの役割などを知りたい場合は Quora: クライエント側のバリデーションとサーバーサイド側のバリデーションをどのような点を意識して、使い分けますか? が詳しい。
関連リンク