仕組み
@k8ordo/formがどう動いているかを説明します。使い方を覚えるのに必要な内容ではありませんが、なぜそう書くのかが分かると、迷ったときに判断しやすくなります。
このページの内容
1つのスキーマが3か所で働く
スキーマを使うのは、サーバーで入力欄の情報を作るときと、サーバーで送信を検証するときの2回です。ブラウザが受け取るのは、そこから作ったJSONだけです。
server formFields(schema) → { fields, arrays, rules, dropped }
↓ props (JSON)
client useForm(fields, state) → props for <form>, input for each field
↓ submit
server parseForm(schema, data) → typed data, or errors per fieldそのため、zodはブラウザのバンドルに入りません。スキーマはzodでもzod/miniでも書けますが、ブラウザから読まないなら、どちらを選んでもバンドルの大きさは変わりません。
値はDOMが持つ
入力された値は、Reactのstateに写さずにDOMに置いたままにします。stateに持つのは、DOMでは表せない次の5つだけです。
- ブラウザが失敗と判断した入力欄に、どの文言を出すか
- サーバーが返したエラーのうち、まだ直していないものはどれか
- 繰り返しの行のそれぞれを見分けるキー
- DOMから読み直した、変更があるかどうかの1つの真偽値
- 行の追加や削除を比べるための、もとの行数
値を写さないので、キーを押すたびに再描画されることはありません。また、ブラウザのリセットやReactがアクションのあとに行うリセットも、そのまま正しく働きます。
JavaScriptが無くても動く理由
入力欄の制約は、サーバーが描いたHTMLの属性に入っています。そのため、JavaScriptが届く前でもブラウザ自身が入力を確かめます。
JavaScriptが届くと、useFormはフォームにnoValidateを付け、同じ検証をzodの文言で行います。noValidateはHTMLには書かずに後から付けるので、JavaScriptが無い環境ではブラウザの検証が残ります。
送信に失敗したときは、送信した値が入力欄のdefaultValueとして描き直されます。JavaScriptが無い環境でも、入力した内容は失われません。
文言がブラウザとサーバーで食い違わない理由
formFieldsは、検証の種類ごとに、その検証だけが失敗する値をスキーマに渡し、返ってきたエラーの文言を取り出します。ブラウザで表示する文言は、zodがサーバーで出す文言そのものです。
requiredの決まり方
JSON Schemaのrequiredは「キーがある」という意味ですが、フォームはどの入力欄からも何かしらを送ります。そこでformFieldsは、その入力欄が空のときに送る値をスキーマが拒むかどうかで、requiredを付けるかを決めます。
空のときに送る値は、入力欄によって違います。テキストは""、チェックの無いチェックボックスはfalseです。数値やファイル、選択肢は何も送りません。parseFormも同じ値をスキーマに渡すので、ブラウザとサーバーの判断がそろいます。下の例では、空の文字列を受け付けるbioと、空欄を省けるageにはrequiredが付きません。
z.object({ bio: z.string() });
// bio: { name: 'bio', type: 'text' }
z.object({ title: z.string().min(1) });
// title: { name: 'title', required: true, type: 'text', minLength: 1 }
z.object({ age: z.coerce.number().optional() });
// age: { name: 'age', type: 'number', step: 'any' }ブラウザで確かめられない検証を知らせる
スキーマ全体に付けた.refine()や、フラグ付きの正規表現のように、HTMLの属性で表せない検証があります。formFieldsはこうした検証を黙って捨てずに、droppedに並べて返します。
本番環境以外では、console.warnでも一覧を出します。どの検証もサーバーでは必ず行われるので、ブラウザで確かめないだけです。
フォームで表せないスキーマはエラーにする
フォームが送る値はいつも文字列なので、z.number()やz.date()はどんな入力も受け付けない入力欄になります。z.recordやタプルのように、送信する名前が決まらない形もあります。formFieldsは、こうしたスキーマを受け取ると理由を添えてエラーを投げます。
入力した内容を黙って捨てたり、決して通らないフォームを作ったりするのは、間違いとして知らせるべきだと考えているからです。