TanStack Formで巨大フォームを安全に分割する方法
1. はじめに
フロントエンドアプリケーションで、たくさんの入力項目を扱う巨大なフォームを構築する場面は少なくありません。
1つのフォームコンポーネント内に10個以上のフィールドが並び、それぞれにバリデーションや依存関係、条件分岐があるようなケースでは、コードはすぐに読みにくくなり、ちょっとした修正でも壊れやすくなってしまいます。
こうした問題を避けるためには、フォームを適切な単位で分割することが重要です。TanStack Form では withForm
を使うことで、型安全かつ再利用性の高いフォーム分割が実現できます。
この記事では、TanStack Form の withForm
を活用して、巨大フォームをどのように構造化・分割できるかを具体的に解説していきます。
2. TanStack Form における分割戦略の全体像
TanStack Form でフォームを分割する基本戦略は以下の通りです:
-
親フォーム:アプリケーションの主要な
useForm
インスタンスを保持し、送信処理や全体的なバリデーションを担当する -
子フォーム:
withForm
によって定義されたフォーム単位のコンポーネント。親から渡された form を使って、自身が担当するフィールドの定義とUIを持つ
このような構造にすることで、
- UIの再利用性が高まる
- 各コンポーネントの責務が明確になる
- メンテナンス性が向上する
というメリットがあります。
子フォームは、フォームをセクション単位に分割するイメージで考えると分かりやすいです。
たとえば「名前入力」「住所入力」「支払い情報」など、意味的にまとまりのある単位ごとにフォームを切り出します。
具体的には以下のような構成になります:
<UserForm> // 親フォーム
<UserNameForm /> // 子フォーム1(名前セクション)
<UserAddressForm /> // 子フォーム2(住所セクション)
<UserPaymentForm /> // 子フォーム3(支払いセクション)
</UserForm>
それぞれの子フォームは、自分が扱うフィールドのみに責任を持ち、UI・バリデーション・状態管理を局所的に完結させることができます。
useAppForm
のセットアップ
3. 基盤となる フォームの共通機能を定義するため、まずは TanStack Form の createFormHookContexts
と createFormHook
を使って自前の useAppForm
を作成します。
// hooks/useForm.ts
import { createFormHookContexts, createFormHook } from "@tanstack/react-form";
const { fieldContext, formContext, useFieldContext, useFormContext } =
createFormHookContexts();
const fieldComponents = {}; // フィールドコンポーネントをここに定義する(例: { TextField, SelectField })
const formComponents = {}; // フォームコンポーネントをここに定義する(例: { SubmitButton })
export const { useAppForm, withForm } = createFormHook({
fieldComponents,
formComponents,
fieldContext,
formContext,
});
このカスタムフックを利用することで、どのフォームコンポーネントでも共通の型とバリデーションの土台が整った状態で開発を始められます。
fieldComponents
には 独自定義したTextField や SelectField などの再利用可能なフィールドコンポーネントを、formComponents
には SubmitButton などのフォーム全体で使用するコンポーネントを定義することで、アプリケーション全体で一貫したフォーム体験を提供できます。
withForm
で切り出す
4. 子フォームを 次に、巨大なフォームの一部を withForm
を使って子コンポーネントとして切り出します。
// features/user/UserNameForm.tsx
import { withForm } from '@/hooks/useForm'
export const UserNameForm = withForm({
// これらの値は型チェックのためのみに使用され、ランタイムでは使用されません
defaultValues: {
firstName: '',
lastName: '',
},
props: { title: '' },
render: function Render({ form, title }) {
return (
<>
<h3>{title}</h3>
<form.Field name="firstName">
{(field) => <input value={field.state.value} onChange={e => field.handleChange(e.target.value)} />}
</form.Field>
</>
)
},
})
このように withForm
を使うと、親から渡された form
オブジェクトを使って子側が自由にフィールドをレンダリングできます。
注意点: render
プロパティは、ESLintエラーを避けるために無名関数ではなく名前付き関数として定義することをオススメします。
5. 親フォームとの接続方法
子フォームを実際に使う親コンポーネントでは、以下のように useAppForm
を使って form
インスタンスを生成し、それを子に渡します:
// features/user/UserForm.tsx
import { useAppForm } from '@/hooks/useForm'
import { UserNameForm } from './UserNameForm'
export const UserForm = () => {
const form = useAppForm({
defaultValues: {
firstName: '太郎',
lastName: '山田',
},
onSubmit: async ({ value }) => console.log(value),
})
return (
<form.AppForm>
<UserNameForm form={form} title="名前情報" />
</form.AppForm>
)
}
このように、親フォームと子フォームは form
を通じて明確につながります。form.AppForm
コンポーネントラッパーが必要なコンテキストを提供し、子フォームが親のフォーム状態にアクセスできるようになります。
6. まとめ
TanStack Form の withForm
を使うことで、巨大フォームを安全かつ効率的に分割できます。
- 子フォームに責務を委ねることで、保守性と再利用性が向上する
- 型推論やバリデーションも親から継承され、一貫性を担保できる
-
withForm
のdefaultValues
は型チェックのためのみに使用され、実際の初期値は親フォームから提供される
本記事では触れませんでしたが、この構成からさらに lazy
や Suspense
を組み合わせてページ分割を行ったり、Valibot や Zod など外部スキーマ連携によるバリデーション強化も検討できるでしょう。
複雑なフォームを構築する際は、ぜひ一度 withForm
を取り入れてみてください。
参考リンク

ちょっと株式会社(chot-inc.com)のエンジニアブログです。 フロントエンドエンジニア募集中! カジュアル面接申し込みはこちらから chot-inc.com/recruit/iuj62owig
Discussion