Gemcook Tech Blog
🏖️

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を最大限活かす」「余計な再レンダリングをさせない」といった思想がフォームにもそのまま持ち込まれているのが特徴です。

https://tanstack.com/form/latest

ここからは、公式ドキュメントでも強みとして挙げられているポイントを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.Fieldnameプロパティでは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でこのような再レンダリングの最適化をしようとすると、コンポーネント分割を考えたり、memouseCallbackを使ったりと、意外とコストがかかります。

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には、フォームに入力された値がオブジェクトとしてまとめて渡されます。このvaluedefaultValuesの構造に基づいた型として扱われるため、安全に値を参照できます。
  • 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:このフィールドがどのフォームの値と紐づくかを指定します。useFormdefaultValuesで定義したキーを指定する必要があります。ここは型補完が効くため、打ち間違いを防げます。
  • 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:フィールドのバリデーションルールを定義します。onChangeonBluronSubmitなど、どのタイミングでバリデーションを実行するかを指定できます。
  • 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ライブラリのコンポーネントを使う際にuseControllerControllerを使っていました。

<Controller
  name="status"
  control={control}
  render={({ field }) => <Select {...field} />}
/>

TanStack Formでは、form.Fieldchildren内でカスタムコンポーネントを直接使えます。

<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とは設計思想が異なる部分があるので、プロジェクトの規模や要件に応じて使い分けることが大切です。

この記事が、フォーム実装の選択肢を広げるきっかけになれば嬉しいです。

Gemcook Tech Blog
Gemcook Tech Blog

Discussion