Install
$ agentstack add skill-tanstack-skills-tanstack-skills-tanstack-form ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
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 valibot
Core: 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 (
{
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
{/* Fields */}
({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}
children={({ canSubmit, isSubmitting }) => (
{isSubmitting ? 'Submitting...' : 'Submit'}
)}
/>
)
}
Fields (form.Field)
value.length (
First Name
field.handleChange(e.target.value)}
/>
{field.state.meta.isTouched && field.state.meta.errors.length > 0 && (
{field.state.meta.errors.join(', ')}
)}
)}
/>
{(field) => (
field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
)}
Validation
Validation Timing
| Cause | When | |-------|------| | onChange | After every value change | | onBlur | When field loses focus | | onSubmit | During submission | | onMount | When field mounts |
Synchronous Validation
{
if (value {
if (!value) return 'Required'
return undefined
},
}}
/>
Asynchronous Validation
{
const res = await fetch(`/api/check-username?q=${value}`)
const { available } = await res.json()
if (!available) return 'Username taken'
return undefined
},
}}
>
{(field) => (
<>
field.handleChange(e.target.value)} />
{field.state.meta.isValidating && Checking...}
)}
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-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
{
const password = fieldApi.form.getFieldValue('password')
if (value !== password) return 'Passwords do not match'
return undefined
},
}}
/>
Array Fields
{(field) => (
{field.state.value.map((_, index) => (
{(subField) => (
subField.handleChange(e.target.value)}
/>
)}
field.removeValue(index)}>
Remove
))}
field.pushValue({ name: '', age: 0 })}>
Add Person
)}
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 position
Listeners (Side Effects)
{
// Side effect: reset dependent fields
form.setFieldValue('state', '')
form.setFieldValue('postalCode', '')
},
}}
/>
Reactivity (form.Subscribe & useStore)
// Render-prop subscription (fine-grained)
({ canSubmit: state.canSubmit, isDirty: state.isDirty })}
children={({ canSubmit, isDirty }) => (
{isDirty && Unsaved changes}
Save
)}
/>
// Hook-based subscription
function FormStatus() {
const isValid = form.useStore((s) => s.isValid)
return isValid ? null : Fix errors
}
Form State
interface FormState {
values: TFormData
errors: ValidationError[]
errorMap: Record
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 {
value: TData
meta: {
isTouched: boolean
isDirty: boolean
isPristine: boolean
isValidating: boolean
errors: ValidationError[]
errorMap: Record
}
}
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
}
// TypeScript auto-completes all valid paths:
// 'name', 'address', 'address.street', 'address.city', 'tags', 'contacts'
// OK
// 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)
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: tanstack-skills
- Source: tanstack-skills/tanstack-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.