🔍

「レンズ」でフォーカス!@hookform/lensesを使ったフォーム実装

こんにちは、フォルシア株式会社エンジニアの籏野です。

先日、React Hook Formの公式アカウントが以下のようなポストをしているのを見つけました。

https://x.com/HookForm/status/1894698099677028618

新たにリリースされた@hookform/lensesがどのようなライブラリなのか気になり調べてみました。

@hookform/lensesとは

GitHubに記載の内容を訳すると以下のようになります。(日本語訳 by DeepL)

React Hook Form Lensesは、React Hook Formに関数型レンズのエレガンスをもたらす、強力なTypeScriptファーストのライブラリです。
入れ子になったフォームの状態を型安全に操作できるため、開発者は複雑なフォームデータを正確に制御し、簡単に変換することができます。
このライブラリのコンポーザブルなレンズ操作により、型安全性を維持したまま深くネストされた構造を簡単に扱うことができ、より保守性と再利用性の高いフォームコンポーネントを実現できる。

まとめると本ライブラリのポイントは以下になりそうです。

  • 関数型レンズをReact Hook Formに導入
  • 入れ子になっているような複雑なフォームに対して、型安全に保守性と再現性の高いコンポーネントを実現

実際に簡単なフォームを実装して、これらがどのような効果を発揮しているのかを確認したいと思います。

実装してみた

以下のリポジトリに実装内容をまとめていますので、参考にしてください。

https://github.com/taku-hatano/lenses-practice

今回作成したフォームは以下のような見た目になっています。

@hookform/lensesのREADMEに記載されているコンポーネントをshadcn/uiで実装し直した形になっており、人の姓名を入力するPersonFormを繰り返し再利用するようなフォームになっています。
なお、入力の結果得られるデータの型は以下のようになっています。

interface FormSchema {
	firstName: string;
	lastName: string;
	children: {
		name: string;
		surname: string;
	}[];
}

PersonFormnamesurnameという2つのフィールドを持った"レンズ"を受け取り、それぞれのフィールドに紐づいた入力フォームを描画しています。

function PersonForm({
	lens,
}: {
	lens: Lens<{ name: string; surname: string }>;
}) {
	return (
		<>
			<StringInput lens={lens.focus("name")} label="Name" />
			<StringInput lens={lens.focus("surname")} label="SurName" />
		</>
	);
}

ここで言う"レンズ"とは何でしょうか?
PersonFormの呼び出し元を確認してみます。

<div>
	<TypographyH3>Person</TypographyH3>
	<PersonForm
		lens={lens.reflect((l) => ({
			name: l.focus("firstName"),
			surname: l.focus("lastName"),
		}))}
	/>
</div>

FormSchema型内の、firstNamelastNameに"フォーカス"し、PersonFormname/surnameに渡していることが見て取れます。
このように、親から子へどんどんプロパティをフォーカスしながら渡していくのがレンズたる由縁と言えそうです。

ただこれだけだとイメージが付きにくいかと思いますので、入れ子になったフォームも見ていきます。

	const children = lens.focus("children");
	const { fields: childrenFields } = useFieldArray(children.interop());

	return <>
		...
		{children.map(childrenFields, (l, key, index) => (
			<div key={key}>
				<TypographyH3>Child {index + 1}</TypographyH3>
				<PersonForm lens={l} />
			</div>
		))}
		...
	</>;

上記はchildrenというプロパティ内に配列の形で入れ子になったフォームを表しており、先ほど利用したPersonFormの再利用が行えていることが確認できます。
@hookform/lensesを使わずにこのようなフォームを実装する場合、おそらく以下のようにchildren.1.nameのような長いフィールドパスを指定する必要があったかと思います。

<PersonForm {control.register(`children.${index}.name`)} />

上記のようなフィールドパスを指定する場合、どうしても親要素のフィールドを意識しながら実装する必要があります。
@hookform/lensesを使うことで、このような親要素がどのようなフィールドを持つのかを意識することがなくなり、
childrenにフォーカス→その中の各要素を取得→要素内のnameにフォーカス... といった形で徐々に参照する範囲を狭めていくことができるようになります。
これにより自身がどのようなプロパティを持つのかを定義するだけでコンポーネントを実装できるようになるため、より再利用性の高いフォームコンポーネントを実現できるのではないかと感じました。

まとめ

今回は新たに発表された@hookform/lensesを使ったフォーム実装を試してみました。
lensという概念により、徐々にフォーカスを絞り込んでいくような実装方法は、他の様々な要素への依存を減らすことができるのが魅力のように感じました。
@hookform/lensesは2025/03月時点で0.1.1というバージョンであり、今後さらなる発展にも期待したいですね。

この記事を書いた人

籏野 拓
2018年新卒入社

FORCIA Tech Blog

Discussion