tanstack-form
TS/JSおよびReact、Vue、Angular、Solid、Lit、Svelteに対応した、ヘッドレスで高パフォーマンスかつ型安全なフォーム状態管理ライブラリです。UIに依存しない柔軟な設計により、あらゆるフレームワークで一貫したフォーム管理を実現します。
description の原文を見る
Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, Lit, and Svelte.
SKILL.md 本文
概要
TanStack Form は深い TypeScript 統合を備えたヘッドレス フォームライブラリです。フィールドレベルおよびフォームレベルの検証 (同期/非同期)、配列フィールド、リンク/依存フィールド、細粒度リアクティビティ、およびスキーマ検証アダプタサポート (Zod、Valibot、Yup) を提供します。
Package: @tanstack/react-form
Adapters: @tanstack/zod-form-adapter, @tanstack/valibot-form-adapter
Status: Stable (v1)
インストール
npm install @tanstack/react-form
# オプションのスキーマアダプタ:
npm install @tanstack/zod-form-adapter zod
npm install @tanstack/valibot-form-adapter valibot
コア: useForm
import { useForm } from '@tanstack/react-form'
function MyForm() {
const form = useForm({
defaultValues: {
firstName: '',
lastName: '',
email: '',
age: 0,
},
onSubmit: async ({ value }) => {
// value は完全に型付けされている
await submitToServer(value)
},
onSubmitInvalid: ({ value, formApi }) => {
console.log('バリデーション失敗:', formApi.state.errors)
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
{/* フィールド */}
<form.Subscribe
selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}
children={({ canSubmit, isSubmitting }) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '送信中...' : '送信'}
</button>
)}
/>
</form>
)
}
フィールド (form.Field)
<form.Field
name="firstName"
validators={{
onChange: ({ value }) =>
value.length < 3 ? '最低3文字以上である必要があります' : undefined,
}}
children={(field) => (
<div>
<label htmlFor={field.name}>名前</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.isTouched && field.state.meta.errors.length > 0 && (
<em>{field.state.meta.errors.join(', ')}</em>
)}
</div>
)}
/>
<!-- ネストされたフィールドはドット記法を使用 -->
<form.Field name="address.city">
{(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
)}
</form.Field>
バリデーション
バリデーションのタイミング
| 原因 | 実行時 |
|---|---|
onChange | 値の変更のたびに |
onBlur | フィールドがフォーカスを失ったとき |
onSubmit | 送信中 |
onMount | フィールドがマウントされたとき |
同期バリデーション
<form.Field
name="age"
validators={{
onChange: ({ value }) => {
if (value < 18) return '18歳以上である必要があります'
return undefined // undefined = 有効
},
onBlur: ({ value }) => {
if (!value) return '必須項目です'
return undefined
},
}}
/>
非同期バリデーション
<form.Field
name="username"
asyncDebounceMs={500}
validators={{
onChangeAsync: async ({ value }) => {
const res = await fetch(`/api/check-username?q=${value}`)
const { available } = await res.json()
if (!available) return 'ユーザー名は使用済みです'
return undefined
},
}}
>
{(field) => (
<>
<input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />
{field.state.meta.isValidating && <span>確認中...</span>}
</>
)}
</form.Field>
スキーマバリデーション (Zod)
import { zodValidator } from '@tanstack/zod-form-adapter'
import { z } from 'zod'
const form = useForm({
defaultValues: { email: '', age: 0 },
validatorAdapter: zodValidator(),
onSubmit: async ({ value }) => { /* ... */ },
})
<form.Field
name="email"
validators={{
onChange: z.string().email('無効なメールアドレス'),
onBlur: z.string().min(1, '必須項目です'),
}}
/>
<form.Field
name="age"
validators={{
onChange: z.number().min(18, '18歳以上である必要があります'),
}}
/>
フォームレベルバリデーション
const form = useForm({
defaultValues: { password: '', confirmPassword: '' },
validators: {
onChange: ({ value }) => {
if (value.password !== value.confirmPassword) {
return 'パスワードが一致しません'
}
return undefined
},
},
})
リンク/依存フィールド
<form.Field
name="confirmPassword"
validators={{
onChangeListenTo: ['password'], // パスワード変更時に再バリデーション
onChange: ({ value, fieldApi }) => {
const password = fieldApi.form.getFieldValue('password')
if (value !== password) return 'パスワードが一致しません'
return undefined
},
}}
/>
配列フィールド
<form.Field name="people" mode="array">
{(field) => (
<div>
{field.state.value.map((_, index) => (
<div key={index}>
<form.Field name={`people[${index}].name`}>
{(subField) => (
<input
value={subField.state.value}
onChange={(e) => subField.handleChange(e.target.value)}
/>
)}
</form.Field>
<button type="button" onClick={() => field.removeValue(index)}>
削除
</button>
</div>
))}
<button type="button" onClick={() => field.pushValue({ name: '', age: 0 })}>
追加
</button>
</div>
)}
</form.Field>
配列メソッド
field.pushValue(item) // 末尾に追加
field.insertValue(index, item) // 指定位置に挿入
field.replaceValue(index, item) // 指定位置を置換
field.removeValue(index) // 指定位置を削除
field.swapValues(indexA, indexB) // 位置を入れ替え
field.moveValue(from, to) // 位置を移動
リスナー (副作用)
<form.Field
name="country"
listeners={{
onChange: ({ value }) => {
// 副作用: 依存フィールドをリセット
form.setFieldValue('state', '')
form.setFieldValue('postalCode', '')
},
}}
/>
リアクティビティ (form.Subscribe & useStore)
// レンダープロップサブスクリプション (細粒度)
<form.Subscribe
selector={(state) => ({ canSubmit: state.canSubmit, isDirty: state.isDirty })}
children={({ canSubmit, isDirty }) => (
<div>
{isDirty && <span>未保存の変更があります</span>}
<button disabled={!canSubmit}>保存</button>
</div>
)}
/>
// フックベースのサブスクリプション
function FormStatus() {
const isValid = form.useStore((s) => s.isValid)
return isValid ? null : <p>エラーを修正してください</p>
}
フォーム状態
interface FormState {
values: TFormData
errors: ValidationError[]
errorMap: Record<string, ValidationError>
isFormValid: boolean
isFieldsValid: boolean
isValid: boolean // isFormValid && isFieldsValid
isTouched: boolean
isPristine: boolean
isDirty: boolean
isSubmitting: boolean
isSubmitted: boolean
isSubmitSuccessful: boolean
submissionAttempts: number
canSubmit: boolean // isValid && !isSubmitting
}
フィールド状態
interface FieldState<TData> {
value: TData
meta: {
isTouched: boolean
isDirty: boolean
isPristine: boolean
isValidating: boolean
errors: ValidationError[]
errorMap: Record<ValidationCause, ValidationError>
}
}
FormApi メソッド
form.handleSubmit()
form.reset()
form.getFieldValue(field)
form.setFieldValue(field, value)
form.getFieldMeta(field)
form.setFieldMeta(field, updater)
form.validateAllFields(cause)
form.validateField(field, cause)
form.deleteField(field)
共有フォームオプション (formOptions)
import { formOptions } from '@tanstack/react-form'
const sharedOpts = formOptions({
defaultValues: { firstName: '', lastName: '' },
})
// コンポーネント間で再利用
const form = useForm({
...sharedOpts,
onSubmit: async ({ value }) => { /* ... */ },
})
サーバーサイドバリデーション
// TanStack Start / Next.js サーバーアクション
import { ServerValidateError } from '@tanstack/react-form/nextjs'
export async function validateForm(data: FormData) {
const email = data.get('email') as string
if (await checkEmailExists(email)) {
throw new ServerValidateError({
form: '送信に失敗しました',
fields: { email: 'メールアドレスは既に登録されています' },
})
}
}
TypeScript 統合
// DeepKeys を使用した型安全なフィールドパス
interface UserForm {
name: string
address: { street: string; city: string }
tags: string[]
contacts: Array<{ name: string; phone: string }>
}
// TypeScript がすべての有効なパスを自動補完:
// 'name', 'address', 'address.street', 'address.city', 'tags', 'contacts'
<form.Field name="address.city" /> // OK
<form.Field name="nonexistent" /> // 型エラー!
ベストプラクティス
- フォーム送信時に必ず
e.preventDefault()とe.stopPropagation()を呼び出す - blur バリデーションと isTouched トラッキングのために必ず
onBlur={field.handleBlur}をアタッチする - 配列フィールドには
mode="array"を使用して配列メソッドを取得 - 有効なバリデータに対して
undefinedを返す (null/false ではなく) - 非同期バリデータに
asyncDebounceMsを使用して API スパムを防止 - エラー表示前に
isTouchedをチェックしてより良いUX を実現 form.Subscribeをセレクタで使用して再レンダリングを最小化- コンポーネント間で共有設定に
formOptionsを使用 - 複雑なバリデーションルールにはスキーマバリデータ (Zod/Valibot) を使用
- クロスフィールドバリデーション依存関係に
onChangeListenToを使用
よくある落とし穴
- フォーム送信時に
e.preventDefault()を忘れる (ページが再読み込みされる) - input に
onBlurをアタッチしないこと (blur バリデーションと isTouched が破れる) - 有効なフィールドに対して
nullまたはfalseを返す代わりにundefinedを返さないこと - サブフィールドではなく配列フィールド自体にのみ必要な
mode="array"を誤って使用 - セレクタを使用する代わりにフォーム状態全体をサブスクライブする (不要な再レンダリング)
- 非同期バリデータで
asyncDebounceMsを使用しないこと (キー入力のたびに発火する)
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- tanstack-skills
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/tanstack-skills/tanstack-skills / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。