tanstack-form
Skill3 starsChanged 2 months agoApache-2.0View on GitHub
Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, Lit, and Svelte.
- Installs packages
4 other projects use a version of this, 1 of them edited.
Adopted by
Most recently taken up by
- LaoitdevOpen/laoitdev-skillstheir own version, first seen Oct 20, 2026
- heryTz/skillstheir own version, first seen Oct 13, 2026
- prdlk/skillsword for word, first seen Oct 11, 2026
- Kiranism/next-shadcn-dashboard-starteredited, first seen Oct 8, 2026
Other versions4 projects use a version of this.
Word for word · 1
- prdlk/skills★ 1100% same
Edited · 1
- Kiranism/next-shadcn-dashboard-starter★ 7.2k100% same
Wrote their own version · 2
- heryTz/skills★ 1same name
- LaoitdevOpen/laoitdev-skills★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository28 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .agents/skills/vercel-composition-patterns/AGENTS.mdAGENTS.md
- .agents/skills/vercel-react-best-practices/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- argos-data-importSkill
- argos-releaseSkill
- argos-sddSkill
- awwwards-animationsSkill
- baseline-uiSkill
- bun-file-ioSkill
- design-taste-frontendSkill
- fork-syncSkill
- pi-updateSkill
- react-doctorSkill
- shadcnSkill
- tanstack-pacerSkill
- tanstack-querySkill
- tanstack-rangerSkill
- tanstack-routerSkill
- tanstack-storeSkill
- tanstack-tableSkill
- tanstack-virtualSkill
- turborepoSkill
- vercel-composition-patternsSkill
- vercel-react-best-practicesSkill
- vgpuSkill
- viteSkill
- web-design-guidelinesSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 412
Not attaching onBlur to inputs (breaks blur validation and isTouched)
- Very specific: line 416
Not using asyncDebounceMs with async validators (fires on every keystroke)
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Overview
TanStack Form is a headless form library with deep TypeScript integration. It provides field-level and form-level validation (sync/async), array fields, linked/dependent fields, fine-grained reactivity, and schema validation adapter support (Zod, Valibot, Yup).
Package: @tanstack/react-form
Adapters: @tanstack/zod-form-adapter, @tanstack/valibot-form-adapter
Status: Stable (v1)
Installation
npm install @tanstack/react-form
# Optional schema adapters:
npm install @tanstack/zod-form-adapter zod
npm install @tanstack/valibot-form-adapter valibotCore: useForm
import { useForm } from '@tanstack/react-form'
function MyForm() {
const form = useForm({
defaultValues: {
firstName: '',
lastName: '',
email: '',
age: 0,
},
onSubmit: async ({ value }) => {
// value is fully typed
await submitToServer(value)
},
onSubmitInvalid: ({ value, formApi }) => {
console.log('Validation failed:', formApi.state.errors)
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
{/* Fields */}
<form.Subscribe
selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}
children={({ canSubmit, isSubmitting }) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
)}
/>
</form>
)
}Fields (form.Field)
<form.Field
name="firstName"
validators={{
onChange: ({ value }) =>
value.length < 3 ? 'Must be at least 3 characters' : undefined,
}}
children={(field) => (
<div>
<label htmlFor={field.name}>First 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>
)}
/>
<!-- Nested fields use dot notation -->
<form.Field name="address.city">
{(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
)}
</form.Field>Validation
Validation Timing
| Cause | When |
onChange | After every value change |
onBlur | When field loses focus |
onSubmit | During submission |
onMount | When field mounts |
Synchronous Validation
<form.Field
name="age"
validators={{
onChange: ({ value }) => {
if (value < 18) return 'Must be 18 or older'
return undefined // undefined = valid
},
onBlur: ({ value }) => {
if (!value) return 'Required'
return undefined
},
}}
/>Asynchronous Validation
<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 'Username taken'
return undefined
},
}}
>
{(field) => (
<>
<input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />
{field.state.meta.isValidating && <span>Checking...</span>}
</>
)}
</form.Field>Schema Validation (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('Invalid email'),
onBlur: z.string().min(1, 'Required'),
}}
/>
<form.Field
name="age"
validators={{
onChange: z.number().min(18, 'Must be 18+'),
}}
/>Form-Level Validation
const form = useForm({
defaultValues: { password: '', confirmPassword: '' },
validators: {
onChange: ({ value }) => {
if (value.password !== value.confirmPassword) {
return 'Passwords do not match'
}
return undefined
},
},
})Linked/Dependent Fields
<form.Field
name="confirmPassword"
validators={{
onChangeListenTo: ['password'], // Re-validate when password changes
onChange: ({ value, fieldApi }) => {
const password = fieldApi.form.getFieldValue('password')
if (value !== password) return 'Passwords do not match'
return undefined
},
}}
/>Array Fields
<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)}>
Remove
</button>
</div>
))}
<button type="button" onClick={() => field.pushValue({ name: '', age: 0 })}>
Add Person
</button>
</div>
)}
</form.Field>Array Methods
field.pushValue(item) // Add to end
field.insertValue(index, item) // Insert at index
field.replaceValue(index, item) // Replace at index
field.removeValue(index) // Remove at index
field.swapValues(indexA, indexB) // Swap positions
field.moveValue(from, to) // Move positionListeners (Side Effects)
<form.Field
name="country"
listeners={{
onChange: ({ value }) => {
// Side effect: reset dependent fields
form.setFieldValue('state', '')
form.setFieldValue('postalCode', '')
},
}}
/>Reactivity (form.Subscribe & useStore)
// Render-prop subscription (fine-grained)
<form.Subscribe
selector={(state) => ({ canSubmit: state.canSubmit, isDirty: state.isDirty })}
children={({ canSubmit, isDirty }) => (
<div>
{isDirty && <span>Unsaved changes</span>}
<button disabled={!canSubmit}>Save</button>
</div>
)}
/>
// Hook-based subscription
function FormStatus() {
const isValid = form.useStore((s) => s.isValid)
return isValid ? null : <p>Fix errors</p>
}Form State
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
}Field State
interface FieldState<TData> {
value: TData
meta: {
isTouched: boolean
isDirty: boolean
isPristine: boolean
isValidating: boolean
errors: ValidationError[]
errorMap: Record<ValidationCause, ValidationError>
}
}FormApi Methods
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)Shared Form Options (formOptions)
import { formOptions } from '@tanstack/react-form'
const sharedOpts = formOptions({
defaultValues: { firstName: '', lastName: '' },
})
// Reuse across components
const form = useForm({
...sharedOpts,
onSubmit: async ({ value }) => { /* ... */ },
})Server-Side Validation
// TanStack Start / Next.js server action
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: 'Submission failed',
fields: { email: 'Email already registered' },
})
}
}TypeScript Integration
// Type-safe field paths with DeepKeys
interface UserForm {
name: string
address: { street: string; city: string }
tags: string[]
contacts: Array<{ name: string; phone: string }>
}
// TypeScript auto-completes all valid paths:
// 'name', 'address', 'address.street', 'address.city', 'tags', 'contacts'
<form.Field name="address.city" /> // OK
<form.Field name="nonexistent" /> // Type Error!Best Practices
- Always call
e.preventDefault()ande.stopPropagation()on form submit - Always attach
onBlur={field.handleBlur}for blur validation and isTouched tracking - Use
mode="array"for array fields to get array methods - Return
undefined(not null/false) for valid validators - Use
asyncDebounceMsfor async validators to prevent API spam - Check
isTouchedbefore showing errors for better UX - Use
form.Subscribewith selectors to minimize re-renders - Use
formOptionsfor shared configuration across components - Use schema validators (Zod/Valibot) for complex validation rules
- Use
onChangeListenTofor cross-field validation dependencies
Common Pitfalls
- Forgetting
e.preventDefault()on form submit (causes page reload) - Not attaching
onBlurto inputs (breaks blur validation and isTouched) - Returning
nullorfalseinstead ofundefinedfor valid fields - Using
mode="array"incorrectly (only needed on the array field itself, not sub-fields) - Subscribing to entire form state instead of using selectors (unnecessary re-renders)
- Not using
asyncDebounceMswith async validators (fires on every keystroke)
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

