React Hook Formしか使ってこなかった人のためのTanStack Form
はじめに
Reactのフォームライブラリといえば、多くの人がReact Hook Formを使ってきたと思います。
自分もこれまで、Web / React Native問わず、フォームはほぼReact Hook Form一択で実装してきました。
そんな中、TanStack Formという名前を見かけるようになり、
「正直React Hook Formで困ってないんだよな」
と思いつつも、気になって触ってみたのがきっかけです。
この記事では、TanStack Formはどんなライブラリなのか、どんな機能があり、何ができるのかをコードを交えながらReact Hook Formユーザー目線で紹介していきます。
TanStack Formとは何か?
TanStack Formは、フォームの状態管理とバリデーションを型安全かつ高いパフォーマンスで扱うためのライブラリです。
React Hook Formのような「UIとセットで使うタイプのライブラリ」と比べて、フォームロジックそのものに集中できるヘッドレスな設計になっていて、「フォームの状態管理そのもの」を明示的に扱うことができます。
TanStack QueryやTanStack Tableと同じくTanStackファミリーの一員で、「とにかくTypeScriptを最大限活かす」「余計な再レンダリングをさせない」といった思想がフォームにもそのまま持ち込まれているのが特徴です。
ここからは、公式ドキュメントでも強みとして挙げられているポイントを3つ紹介します。
ファーストクラスのTypeScriptサポート
TanStack系ライブラリ全般に言えることですが、TanStack FormもTypeScriptで書かれており、型安全な開発が可能です。また、フィールドの型補完が優秀です。
まず、React Hook Formでの型の扱い方を見てみましょう。
type FormData = {
email: string;
age: number;
};
const { register, handleSubmit } = useForm<FormData>();
<input {...register("email")} />
React Hook Formでは、useFormにジェネリクスで型を渡すことで型安全性を確保します。これはこれで十分機能しますが、型定義とdefaultValuesを別々に書く必要があり、両者の整合性を保つのは開発者の責任になります。
一方、TanStack Formでは、defaultValuesがフォーム全体の基準となるデータ構造になります。
const form = useForm({
defaultValues: {
email: "",
age: 0,
},
});
// 補完が効く
form.setFieldValue("email", "test@example.com");
// 型の違う値や存在しないフィールドはコンパイルエラー
form.setFieldValue("age", "test@example.com"); // エラー: string型をnumber型に代入できない
form.setFieldValue("name", "test"); // エラー: 'name'というフィールドは存在しない
TanStack Formの特徴は、defaultValuesから型が自動で推論される点です。別途型定義を書く必要がなく、フィールド名・値・バリデーションの型がすべて自動で効きます。
また、フィールド名の補完も優秀で、form.FieldのnameプロパティではdefaultValuesで定義したキーが候補として表示されるため、打ち間違いを防げます。
<form.Field
name="email" // "email" | "age" の補完が効く
children={(field) => (
<input
value={field.state.value} // string型として推論される
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
この仕組みのおかげで、フォームが大きくなっても型が崩れにくく、リファクタリング時の安全性も高まります。例えば、defaultValuesのフィールド名を変更すれば、それを使っている箇所すべてでTypeScriptのエラーが出るため、変更漏れを防げます。
型定義とデータ構造が一致していることが保証されるため、TypeScriptの恩恵を最大限受けられる設計になっています。
ヘッドレスでフレームワークに依存しない
TanStack Formは、UIを一切持たないヘッドレス設計になっており、フォームに必要なロジックのみを提供します。
各フレームワーク間で一部機能は異なりますが、コアのAPIは共通になっているため、フレームワークに影響されることなく技術選定することが可能です。
現在サポートされているフレームワークは以下の通りです。
- React
- Vue
- Angular
- Solid
- Lit
- Svelte
きめ細かなリアクティブパフォーマンス
フォームに値が入力されたとき、その変更は変換後の値に関連するフィールドのみに伝わるようになっており、フォーム全体や関係のないフィールドまで再レンダリングされることはありません。
Reactでこのような再レンダリングの最適化をしようとすると、コンポーネント分割を考えたり、memoやuseCallbackを使ったりと、意外とコストがかかります。
TanStack Formでは、こうした最適化をライブラリ側が自動でやってくれるため、開発者はパフォーマンスを強く意識せず、フォームの実装そのものに集中できます。
基本的な使い方
ここでは、TanStack Formを動かすための基本的な使い方を紹介します。
useFormでフォームを作る
まずはuseFormを使ってフォームを定義します。
ここでフォーム全体の初期値、submit時の処理をまとめて設定します。
import { useForm } from "@tanstack/react-form";
export default function App() {
const form = useForm({
defaultValues: {
name: "",
email: "",
},
onSubmit: async ({ value }) => {
console.log(value);
},
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
{/* Field はここで定義 */}
</form>
);
}
-
defaultValues:フォーム全体の初期値を定義します。ここで指定したキーがフィールドのnameプロパティの値になります。 -
onSubmit:フォームが送信されたときに実行される処理です。引数のvalueには、フォームに入力された値がオブジェクトとしてまとめて渡されます。このvalueはdefaultValuesの構造に基づいた型として扱われるため、安全に値を参照できます。 -
handleSubmit():フォームのsubmit処理を実行するための関数です。<form>のonSubmit内で呼び出すことで、useFormに設定したonSubmitが実行されます。
form.Fieldでフィールドを定義する
次に、フォーム内の各入力項目をform.Fieldで定義します。
TanStack Formでは、入力項目ごとにFieldを作り、フィールド単位で状態とロジックを管理するのが基本です。
<form.Field
name="name"
children={(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
-
name:このフィールドがどのフォームの値と紐づくかを指定します。useFormのdefaultValuesで定義したキーを指定する必要があります。ここは型補完が効くため、打ち間違いを防げます。 -
children:フォームのinputを入れてUIを構築します。引数としてfieldというオブジェクトを受け取れます。このfieldには、そのフィールド専用の状態と操作用のAPIがまとめられています。 -
field.state.value:現在のフィールドの値です。<input>のvalueにそのまま渡すことで、制御コンポーネントとして扱えます。 -
field.handleChange():フィールドの値を更新するための関数です。onChangeで新しい値を渡すだけで、フォームの状態が更新されます。
バリデーションを設定する
TanStack Formでは、フィールドごとにバリデーションルールを定義できます。
ここではnameフィールドに対して、「未入力の場合はエラーにする」バリデーションを設定します。
<form.Field
name="name"
validators={{
onChange: ({ value }) =>
!value ? "名前を入力してください" : undefined,
}}
children={(field) => (
<div>
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
{!field.state.meta.isValid && (
<p>{field.state.meta.errors.join(", ")}</p>
)}
</div>
)}
/>
-
validators:フィールドのバリデーションルールを定義します。onChange・onBlur・onSubmitなど、どのタイミングでバリデーションを実行するかを指定できます。 -
field.state.meta.isValid:そのフィールドが有効かどうかを表すフラグです。falseのときにエラーメッセージを表示することで、「エラーがあるときだけ表示する」UIをシンプルに実装できます。 -
field.state.meta.errors:バリデーションで返されたエラーメッセージは、field.state.meta.errorsに配列として格納されます。
React Hook Form経験者が最初に戸惑うポイント
React Hook Formに慣れている自分がTanStack Formを触ったとき、「いつもの書き方ができない」と感じた場面がいくつかありました。
ここでは、そんな戸惑いポイントと、TanStack Formではどう書くのかをまとめます。
register()がない
React Hook Formでは、register()を使ってフィールドを登録するのが基本でした。
const { register } = useForm();
<input {...register("name")} />
TanStack Formにはregister()に相当するものがなく、代わりにform.Fieldコンポーネントを使います。
<form.Field name="name" children={(field) => <input {...} />} />
最初は「冗長だな」と感じましたが、fieldオブジェクトを通じてフィールドの状態やメタ情報にアクセスできるため、細かい制御がしやすいメリットがあると感じました。
watch()の代わりはform.useStore()
React Hook Formでは、フォームの値を監視するためにwatch()を使っていました。
const email = watch("email");
TanStack Formでは、form.useStore()を使ってフォームの状態を購読します。
const email = form.useStore((state) => state.values.email);
この書き方の良いところは、必要な値だけを購読できる点です。例えば、emailだけを監視している場合、他のフィールドが変更されてもこのコンポーネントは再レンダリングされません。これがTanStack Formのパフォーマンス最適化の仕組みです。
エラーメッセージの取得方法
React Hook Formでは、エラーメッセージをerrorsオブジェクトから取得していました。
const { formState: { errors } } = useForm();
{errors.name?.message}
TanStack Formでは、エラーは各フィールドのstate.metaに含まれています。
{field.state.meta.errors[0]}
エラーは配列で返されるため、errors[0]のように取得します。
フォーム全体のエラー状態を確認したい場合は、form.useStore()を使います。
const canSubmit = form.useStore((state) => state.canSubmit);
useControllerの代わりは?
React Hook Formでは、カスタムコンポーネントやUIライブラリのコンポーネントを使う際にuseControllerやControllerを使っていました。
<Controller
name="status"
control={control}
render={({ field }) => <Select {...field} />}
/>
TanStack Formでは、form.Fieldのchildren内でカスタムコンポーネントを直接使えます。
<form.Field
name="status"
children={(field) => <Select value={field.state.value} onChange={field.handleChange} />}
/>
useControllerに相当する特別なAPIは必要なく、form.Fieldの仕組みがそのまま使えるため、覚えることが少なくて済みます。
どんな人にTanStack Formが向いているか
ここまでTanStack Formを触ってみて、「じゃあ自分は今後これを使うのか?」を考えてみました。
TypeScriptを重視する開発
TanStack Formは、defaultValuesを起点にした型推論が非常に優秀です。フィールド名の補完、値の型チェック、バリデーションの型安全性が全て自動で効くため、TypeScriptでの開発体験を重視するなら大きなメリットがあります。
大規模で複雑なフォーム
フィールドが多く、バリデーションが複雑なフォームを扱う場合、TanStack Formの「フィールド単位での状態管理」と「自動最適化される再レンダリング」が強みになります。React Hook Formでもパフォーマンスは十分ですが、TanStack Formはライブラリ側が自動で最適化してくれるため、開発者が意識しなくても高速に動きます。
フォームのロジックを明示的に扱いたい
TanStack Formはヘッドレス設計で、「フォームの状態管理」そのものに集中できます。UIライブラリやデザインシステムと組み合わせる際に、フォームロジックとUIを明確に分離したい場合に適しています。
まとめ
この記事では、React Hook Formを使ってきた自分がTanStack Formを触ってみた体験をまとめました。
TanStack Formは、TypeScriptの型安全性・パフォーマンスの最適化・フレームワークに依存しない設計といった特徴を持つ、モダンなフォームライブラリです。ただ、React Hook Formとは設計思想が異なる部分があるので、プロジェクトの規模や要件に応じて使い分けることが大切です。
この記事が、フォーム実装の選択肢を広げるきっかけになれば嬉しいです。
Discussion