tanstack-form
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 (
<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 position
Listeners (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)
More from kiranism/next-shadcn-dashboard-starter
tanstack-query
TanStack Query v5 data fetching patterns including useSuspenseQuery, useQuery, mutations, cache management, and API service integration. Use when fetching data, managing server state, or working with TanStack Query hooks.
14next-best-practices
Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling
6shadcn
Manages shadcn components and projects — adding, searching, fixing, debugging, styling, and composing UI. Provides project context, component docs, and usage examples. Applies when working with shadcn/ui, component registries, presets, --preset codes, or any project with a components.json file. Also triggers for "shadcn init", "create an app with --preset", or "switch to --preset".
5frontend-design
Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
5web-design-guidelines
Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
5vercel-react-best-practices
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
5