web-forms-tanstack-form について
TanStack Formでフォームを構築するためのパターンガイドです。Reactなどの対応フレームワークを使う開発者向けに、型推論、イベント別検証、配列フィールド、共有コンポーネント、状態購読を説明します。
主な機能
- 01`defaultValues`からフィールドと送信型を推論
- 02`form.Field`によるフィールド描画
- 03イベント別の同期・非同期検証
- 04連動フィールドと動的配列の処理
- 05`createFormHook`によるコンポーネント再利用
- 06状態セレクターによる再描画の抑制
活用シーン
- 01検証付きの型付きフォーム構築
- 02関連値の変更時にフィールドを再検証
- 03配列フィールド群の追加や削除
- 04アプリ内でフォームコンポーネントを共有
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-forms-tanstack-formインストール後、Claude Code で/web-forms-tanstack-formを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.yamlreference.mdexamples/arrays.mdcomposition.mdcore.mdvalidation.md詳細
TanStack Form のパターン
クイックガイド:
useFormはdefaultValuesを受け取り、すべてのフィールド名、値の型、送信ペイロードはそのオブジェクトから推論されます。フィールドはform.Fieldを通じてレンダリングされ、childrenレンダープロパティからfield.state.value、field.handleChange、field.handleBlurが提供されます。バリデーションはvalidatorsプロパティに記述します。イベント(onChange、onBlur、onSubmit)ごとに設定でき、それぞれにAsyncバリアントがあり、フィールドまたはフォームに設定できます。mode="array"を使うとpushValue/removeValueが利用でき、onChangeListenToを使うと別のフィールドが変更されたときにバリデーターが再実行され、form.Subscribeを使うと、どの状態変更によって何が再レンダリングされるかを絞り込めます。
詳細リソース:
- examples/core.md — フィールド、型付け、送信、リセットを含むフォームの全体的な例
- examples/validation.md — 同期、非同期、フィールド間バリデーター、バリデーター内のスキーマオブジェクト
- examples/arrays.md —
mode="array"を使った動的なフィールドグループ - examples/composition.md —
createFormHook、useAppForm、リスナー - reference.md — バリデーターイベント、フィールドとフォームの状態の表、API メソッド、フレームワークのパッケージ
該当する方法
- 単一のフォーム —
useFormとform.Fieldのレンダープロパティだけで設定できます。それ以外の設定は不要です。 examples/core.md を参照してください。 - アプリ全体で同じ動作にしたいフォーム —
createFormHookで共有するフィールドコンポーネントとフォームコンポーネントを一度登録し、各呼び出し箇所でuseFormをuseAppFormに置き換えます。examples/composition.md を参照してください。 - React 以外のフレームワーク — フォームのコアは共通で、異なるのはパッケージとフィールドバインディングだけです。reference.md の「Framework Packages」表に、それぞれの両方が記載されています。
<critical_requirements>
TanStack Form のコードを書く前に
すべてのフィールドについて、useForm に defaultValues のエントリーを指定してください。 フィールド名、値の型、送信ペイロードはすべてこのオブジェクトから推論されるため、ここにないフィールドは型に認識されません。
すべてのフィールドを form.Field とその children レンダープロパティを通じて描画してください。 レンダープロパティには値とハンドラーが明示的に渡されます。このライブラリにはフィールド登録用のヘルパーがなく、ref の転送も行いません。そのため、別の方法で接続した入力はフォームに参加しません。
検証は、実行するイベントをキーにして validators プロパティに指定してください。 onChange、onBlur、onSubmit にはそれぞれ Async 版があり、同じプロパティをフィールドとフォームの両方で使えます。
field.state.meta.errors は配列として読み取ってください。 ここにはフィールドの現在のエラーがすべて含まれているため、.map() で処理するか、.length を確認してください。文字列と比較すると、常に一致しません。
form.handleSubmit() を呼び出す前に、フォームの onSubmit で e.preventDefault() を呼び出してください。 このライブラリはネイティブの送信をインターセプトしないため、これを行わないと送信の途中でブラウザーが画面遷移します。
</critical_requirements>
自動検出: @tanstack/react-form、@tanstack/vue-form、@tanstack/solid-form、 @tanstack/angular-form、@tanstack/lit-form、@tanstack/form-core、form.Field、form.Subscribe、 createFormHook、createFormHookContexts、useAppForm、withForm、fieldContext、formContext、 field.handleChange、field.handleBlur、field.state.meta、pushValue、removeValue、swapValues、 onChangeListenTo、onBlurListenTo、setErrorMap、formDevtoolsPlugin
対象範囲:
- フォームの状態、検証のタイミング、送信
- あるフィールドの有効性が別のフィールドの値に依存する、フィールド間のルール
- 追加、削除、並べ替えができるフィールドグループの動的なリスト
- ファクトリーを通じた、アプリ内で共有するフィールドおよびフォームコンポーネント
- React だけでなく、Vue、Solid、Angular、Lit のフォーム
別の場所で扱う内容:
- 検証スキーマの作成 — バリデーターは任意の Standard Schema オブジェクトを受け取ります。そのスキーマがルールをどのように記述するかは、それを所有する側で決まります。
- 入力の描画とスタイリング — このライブラリは UI を所有しません。レンダープロパティから値とハンドラーが渡され、マークアップは自分で用意します。
- 初期値の取得元 —
defaultValuesはプレーンなオブジェクトで、フォームは何も取得しません。
フォームはヘッドレスで、型は推論に基づいています。defaultValues が基準となるスキーマです。フィールド名はここからオートコンプリートされ、field.state.value の型もここから決まり、onSubmit のペイロードもこれに一致します。ジェネリック型パラメーターも、内容がずれる可能性のある二つ目の型宣言も必要ありません。
検証はモードではなくイベントに結び付けられます。各バリデーターは、それが属するレベルで実行タイミングを宣言します。これにより、簡単な形式チェックを onChange に設定し、コストの高い一意性チェックは同じフィールドの onBlurAsync まで待たせることができます。
状態はサブスクリプションを通じて読み取ります。form.Subscribe と useStore はセレクターを受け取り、その戻り値が変化した場合にのみ再レンダリングするため、コンポーネント本体で form.state を直接読み取ると、この設計全体が無効になります。
コアパターン
パターン 1: useForm と form.Field
レンダープロパティがフィールド API のすべてです。値を受け取り、ハンドラーを渡します。暗黙の処理はありません。
const form = useForm({
defaultValues: { name: "", email: "" },
onSubmit: async ({ value }) => {
await submitToApi(value);
},
});
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.Field
name="email"
children={(field) => (
<input
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
</form>;
onBlur={field.handleBlur} を指定すると、フィールドがタッチ済みとしてマークされます。これを省略すると、isTouched は false のままとなり、onBlur バリデーターも実行されません。
完全なコード: examples/core.md
パターン 2: フィールドレベルのバリデーター
同期バリデーターは、値が有効な場合にエラーメッセージ文字列、または undefined を返します。
<form.Field
name="age"
validators={{
onChange: ({ value }) => (value < 13 ? "Must be 13 or older" : undefined),
onBlurAsync: async ({ value }) => {
const ok = await checkAge(value);
return ok ? undefined : "Age not valid on server";
},
}}
children={(field) => (/* ... */)}
/>
同期検証が非同期検証を制御します。onBlur と onBlurAsync の両方がある場合、非同期検証は同期検証に合格した後にのみ実行されます。そのため、すでに無効とわかっている値に対してネットワークリクエストが発生することはありません。
完全なコード: examples/validation.md
パターン 3:連動するフィールド
onChangeListenTo には、このフィールドのバリデーターを再実行する変更対象のフィールドを指定します。
<form.Field
name="confirm_password"
validators={{
onChangeListenTo: ["password"],
onChange: ({ value, fieldApi }) =>
value !== fieldApi.form.getFieldValue("password")
? "Passwords do not match"
: undefined,
}}
children={(field) => (/* ... */)}
/>
これを指定しないと、password を編集しても confirm_password のエラーには古い比較結果が表示されたままになり、ユーザーが確認用フィールドに再度触れるまで更新されません。
コード全体:examples/validation.md
パターン 4:配列フィールド
mode="array" を指定すると、フィールドに pushValue、removeValue、insertValue、swapValues、moveValue が追加されます。ネストされたフィールドでは、インデックスで項目を指定します。
<form.Field
name="hobbies"
mode="array"
children={(hobbies) => (
<div>
{hobbies.state.value.map((_, i) => (
<form.Field
key={i}
name={`hobbies[${i}].name`}
children={(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
))}
<button type="button" onClick={() => hobbies.pushValue({ name: "" })}>
Add hobby
</button>
</div>
)}
/>
コード全体:examples/arrays.md
パターン 5:フォームレベルのバリデーター
useForm のバリデーターはすべての値をまとめて参照できるため、サーバーサイドのバリデーションを行うのに適しています。個々のフィールドにエラーを割り当てられるためです。
const form = useForm({
defaultValues: { username: "", age: 0 },
validators: {
onSubmitAsync: async ({ value }) => {
const errors = await validateOnServer(value);
if (!errors) return null;
return {
form: "Submission failed",
fields: { username: errors.username, age: errors.age },
};
},
},
});
戻り値の形式は { form?: string, fields: Record<string, string> } で、null は有効であることを意味します。文字列をそのまま返すフィールドバリデーターとは異なります。
コード全体:examples/validation.md
パターン 6:createFormHook
ファクトリーにフィールドコンポーネントとフォームコンポーネントを一度登録しておくと、各フォームから form.AppField と form.AppForm として利用できるため、レンダープロップのマークアップを繰り返す必要がありません。
export const { fieldContext, formContext, useFieldContext } =
createFormHookContexts();
export const { useAppForm, withForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: { TextField, SelectField },
formComponents: { SubmitButton },
});
useAppForm は useForm が受け取るすべてのものを受け取ります。
コード全体:examples/composition.md
パターン 7:リスナー
リスナーはフィールドのイベントに反応して副作用を実行します。何も返しません。エラーを返すのはバリデーターです。
<form.Field
name="country"
listeners={{
onChange: () => form.setFieldValue("province", ""),
}}
children={(field) => (/* ... */)}
/>
利用可能なイベント:onChange、onBlur、onMount、onSubmit。
コード全体:examples/composition.md
パターン 8:form.Subscribe
再レンダリングの対象は selector で決まります。実際にレンダリングする状態だけに絞り込みます。
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting] as const}
children={([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit || isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit"}
</button>
)}
/>
コード全体:examples/core.md
<red_flags>
注意点
実行時に問題が発生します:
e.preventDefault()を使わずにform.handleSubmit()を呼び出す — ブラウザーがフォームをネイティブに送信し、送信処理の途中でページが再読み込みされます。defaultValuesにフィールドがない — そのフィールドのfield.state.valueはundefinedになり、入力欄は非制御コンポーネントとしてマウントされ、フィールドの型も不明になります。field.state.meta.errorsを文字列として比較する — これは配列なので比較は常に false となり、メッセージは表示されません。.map()を使ってください。- 部分的なオブジェクトを
pushValueに渡す — 配列の要素型に一致せず、キーが欠けていると、対応する入力欄が非制御になります。 onSubmit内でエラーがスローされる —form.handleSubmit()はこれを捕捉しません。コールバック内で捕捉し、form.setErrorMap()でエラーを表示してください。- 配列の項目にドット記法を使う — フィールドパスは
items[0].nameであり、items.0.nameでは何も指定されません。
予想外の動作:
- コンポーネント本体で
form.stateを読み取ると、状態が変わるたびに購読します。form.Subscribeとセレクター、またはuseStore(form.store, selector)を使うと、購読対象を絞り込めます。 form.Subscribeにselectorを指定しないと、すべてを購読するため、同じコストがかかります。- 同期バリデーターが失敗すると、対応する非同期バリデーターは実行されません。これは意図された動作であり、非同期バリデーターだけでは低コストの事前チェックができないことを意味します。
- フォームレベルのバリデーターは
{ form?, fields }を返すのに対し、フィールドバリデーターは文字列を返します。フォームレベルで返したフィールドの形式は、何も警告されず無視されます。 createFormHookで登録されたコンポーネントはform.AppFieldとform.AppFormに配置されます。form.Fieldも引き続き存在し、通常のレンダープロップを受け取ります。
こうしたよくある問題のうち、特に多いものについては、reference.md に修正前と修正後のコード例があります。
</red_flags>