TypeScript, react-hook-form, zod: blissful formsの翻訳です。
2023年8月8日
TypeScript, react-hook-form, zod: 至福のフォーム
Aivenのオープンソースエンジニアはどのようにフォームを作成し、信頼性が高く、コンポーザブルで楽しいWebフォームを作成しているか
数ヶ月前、Klawはフロントエンドの書き換えを開始し、Angular 1からReactに移行した。今日のフロントエンドの世界には、これほど多種多様な素晴らしいツールがあるのですから。私たちが最初に決断しなければならなかったことのひとつは、フォームをどうするかということでした。
すべてはフォームについて
Klawは、Apache Kafka®トピック用のガバナンス・ツールです。このツールの目的は、4つの目の原則を適用することで、クラスタ上の全てのオペレーションに安全なレイヤーを提供することです。つまり、操作を直接実行する代わりに、ワークフローは操作のリクエストを作成し、それを実行する前にチームの他のメンバーがレビューして承認することになります。ソース管理とコードレビューの作業に少し似ている!
したがって、Klawはリクエストの作成に重点を置いています。トピック、コネクター、スキーマ、サブスクリプションの作成、削除、プロモートのリクエストです。つまり、Klawには多くのフォームがあり、ユーザーが実行したい操作の種類ごとに1つずつ、強力なバリデーションを実装することでユーザーが使いやすいように調整されています。そしてKlawはオープンソースのプロジェクトであるため、そのすべてが将来の機能のために拡張可能である必要があり、また貢献したい人にとって理解しやすいものである必要があります。
したがって、以下のようなフォームを作成するツールが必要です:
- 信頼性: **Klawのコア機能のドライバーとして、厳密な型安全性と確かな検証機能が必要です。
- コンポーザブル:** 複雑なフォームがあるため、自由に構築できるシンプルなビルディング・ブロックが必要です。
- ユーザーと開発者は、私たちのフォームに多くの時間を費やすでしょう。良い時間であるに越したことはありません!
ありがたいことに、新しいKlawフロントエンドはTypeScriptを使用しているため、zodとreact-hook-formという強力なコンビを利用することができます。
zodとは?
zod は "TypeScript-first schema declaration and validation library" である。
zodは、開発者がオブジェクトの表現(「スキーマ」)を構築し、そこからTypeScriptの型を推測することを可能にするプリミティブのライブラリである。また、与えられたオブジェクトに格納された値をスキーマに照らして検証するツールも提供する。
多くの用途があるが、特にフォームのバリデーションに適しており、react-hook-formの作成者はそのことをよく知っている。
react-hook-form`とは?
react-hook-formは、"パフォーマンスが高く、柔軟で拡張可能なフォームと使いやすいバリデーション" を提供します。
react-hook-formは uncontrolled components アプローチをデフォルトとし、HTML標準に従ってフォームを作成するフォームライブラリである。react-hook-form`を使用すると、HTML標準に準拠しているためフォームにアクセスしやすく、ライブラリは使い慣れたAPIを提供する。しかし、非常に寛容で、制御された入力のサポートを含む、柔軟で拡張可能なフックツールボックスを備えている。TypeScriptの強力なサポートにより、開発者はAPIを好きなように使うことができる。
これは私たちにとって、さまざまな意味で重要なことです:
- フォームの構成要素を簡単に書くことができる。
- 入力コンポーネントを制御するかしないかを自由に選択できる。
- そして最も重要なことは、resolver APIを活用することで、
zodを検証ツールとして使えることである。
どう使うか
Klawチームは、react-hook-formとzodを別々のファイルに分け、フォームをレンダリングするときに一緒にすることにした。
react-hook-form: ビルディングブロックを Form.tsx にエクスポートする。
Form.tsxは、react-hook-formでフォームを構築するために必要なすべてのブロックを定義してエクスポートし、バリデーションのためのzodの統合を考慮に入れています。私たちは内部的にAiven Consoleを構築する際にこのアプローチを使用しており、Klawでも、そうでなければ不整合につながる可能性のあるパターンをカプセル化するという明確な目的のために実装しました。例えば、バリデーションはいつ実行されるのでしょうか?この決定はForm.tsxで一度行われ、アプリケーション内のすべてのフォームに適用されます。この意見を取り入れたアプローチにより、フォームを構築する単一の方法を提供することで、開発者にわかりやすい体験を提供することができます。
ブループリントuseForm
useForm フォームが保持する値を定義し、それらの値を検証する方法です react-hook-formのネイティブフック useForm を独自のカスタムフックでラップします。これにより、さまざまなパラメータに意味のある名前をつけたり (たとえば、リゾルバの代わりにスキーマを指定するなど)、すべてのフォームで共有したいデフォルト値を設定したり (たとえば、バリデーション onTouched など)できるようになります。これは、フォームの処理と検証に必要な値とメソッドを保持するオブジェクトを、渡したスキーマに従って型付けして返します。
土台となるものフォーム
フォームのDOM構造を保持する。これは react-hook-form の FormProvider でラップされた HTML フォーム要素を返す単純なコンポーネントである。FormProviderはuseForm` から返されたオブジェクトの値を props として受け取る。
ブリック: 入力コンポーネント
フォームに求めるさまざまな値やユーザー体験を表現するために、さまざまな入力コンポーネントを定義します。これらはフォームコンポーネントの子としてレンダリングされます。これらはすべて useFormContext フックを呼び出すことで親のフォームに登録され、その戻り値を使って自分自身を登録します。
-
単純入力は制御されません。これらは
registerメソッドにnameを指定して呼び出す ことによって親フォームに登録されます。 -
より複雑な入力は
react-hook-formControllerコンポーネントでラップされます。フォームコンテキストからcontrolの値をcontrolプロパティとしてControllerに渡すことで、親フォームに登録される。
zod: スキーマとバリデーションによるセーフティネットの定義
zodを使用して、各フォームのシェイプ、型、バリデーションを定義するので、関連するフォームが定義されている近く、通常はform-schemasフォルダに配置します。TopicSchema.tsxは、topic-schema-request-form.tsで定義されたスキーマと型を使用してフォームをレンダリングする。
スキーマと推論される型
フォームスキーマは通常、一連のキーと値のペアを保持するオブジェクトである。zodではinfer` メソッドを使って、このスキーマから TypeScript の型を推論することができます。
バリデーション
バリデーションはスキーマ定義の不可欠な一部として行われる。これは非常に柔軟で、必要なルールやその複雑さに応じて、さまざまな方法で行うことができます。
単純なバリデーションのために、zod は .min, .max, .optional, .required といった一連のメソッドを提供している。これらのメソッドには常に 2 番目のパラメータが渡され、ディスパッチするエラーメッセージを指定することができる。
より複雑なケース、例えば値が他のフィールドの値を知っていることに依存している場合、 zod は refine メソッドと superRefine メソッドを提供します。これらのメソッドは関数を引数として取り、その関数に特定のケースを処理するためのカスタムロジックを含めることができる。例えば、Klaw でトピックリクエストを作成する際に、 replicationFactor の値が maxReplicatioFactor の値より大きくならないようにすることができる。次に addIssue を使って、この場合にディスパッチしたい正確な種類のエラーを作成します。
ついに一緒に: フォームのレンダリング
これで
自由に構成できるフォームと入力コンポーネント
フォームスキーマ、タイプ、バリデーション
そして、ユーザーに望むUIをレンダリングするために、これらを一緒に使うときが来ました!これがその方法だ:
1.useForm、Form、必要な入力を Form.tsx からインポートする。
2.スキーマと推論される型を定義されている場所からインポートする。
3.推論された型をジェネリック型の引数として useForm を呼び出し、スキーマ (およびオプションのデフォルト値) を渡す。
4.useForm の返された値を Form コンポーネントに props として渡す。
5.フォームスキーマの正しいフィールドに対応する name プロップを渡して、必要な入力を Form の子としてレンダリングする。
概念的には次のようになります:
react-hook-form
zod
react-hook-form + zod
// NameForm.tsx
import { useForm, Form, TextInput } from "src/app/components/Form"
import { formSchema, FormSchema } from "src/app/components/name-form.ts"
// …
const form = useForm<FormSchema>({
schema: formSchema,
defaultValues,
});
<Form
{...form}
>
<TextInput<FormSchema> name={'name-in-schema-type'} /> -> only names in FormSchema allowed as name for TextInput
</Form>
これで目的を達成できたので、もう安心です。私たちのフォームは
- 信頼性: フォームはエンドツーエンドで型付けされ、堅牢なバリデーションが行われます。
- コンポーザブル:フォームの作成は、ビルディング・ブロックを組み合わせることで完結します。
- 喜び: フォームを自信をもって迅速に作成できることは、私たちのチームにとって永遠の喜びです!
欠点
しかしもちろん、あらゆる技術的選択にはトレードオフがあり、これも例外ではない。
制御されていないコンポーネントと制御されたコンポーネントの混合:*。
複雑な入力コンポーネントの多くは制御する必要があります (Controller でラップするか、明示的に value プロパティを設定します)。これはライブラリの基本理念である "HTML標準 "に反しており、複雑さを増しています。この複雑さは当然バグにつながります。このようなバグ。
ちょっとした定型文:
新しい入力タイプをForm.tsxに追加すること、すべてのフォームのためにスキーマと検証ルールを書くこと、正確にすべての入力を入力するために適切な構文を使用することを忘れないこと、このすべてが退屈に感じるかもしれません。このアプローチが強い関心分離を強制するのとは対照的に、コロケーションの甘い快楽に憧れるかもしれない。そしてそれは正しいかもしれない!これは奇跡の解決策ではなく、あなたの目的に合うか合わないかだけの解決策なのだ。
**複雑なUIからは逃れられない。
私たちのアプローチやビルディング・ブロックがシンプルであればあるほど、私たちが構築するUIは時として避けられないほど複雑になります。私たちのACLフォームがそうで、非常に動的で、多くの要因によって有効になったりならなかったりするフィールドがあります。このため、興味深いスキーマとバリデーション、これらのフォームのために特別に設計されたカスタムフィールドのホスト、3000行以上の長さのテストファイルが必要になります。
Klawへの貢献
幸いなことに、これらの欠点から逃れることはできなくても、反復し、改善することによって、それを軽減することができる

