원클릭으로
tanstack-form
// Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, Lit, and Svelte.
// Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, Lit, and Svelte.
Guide for building features, pages, tables, forms, themes, and navigation in this Next.js 16 shadcn dashboard template. Use this skill whenever the user wants to add a new page, create a feature module, build a data table, add a form, configure navigation items, add a theme, set up RBAC access control, or work with the dashboard's patterns and conventions. Also triggers when adding routes under /dashboard, working with Clerk auth/orgs/billing, creating mock APIs, or modifying the sidebar. Even if the user doesn't mention "dashboard" explicitly — if they're adding UI, pages, or features to this project, use this skill.
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.
Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling
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".
Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, edit, or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy.
React composition patterns that scale. Use when refactoring components with boolean prop proliferation, building flexible component libraries, or designing reusable APIs. Triggers on tasks involving compound components, render props, context providers, or component architecture. Includes React 19 API changes.
| name | tanstack-form |
| description | Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, Lit, and Svelte. |
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)
npm install @tanstack/react-form
# Optional schema adapters:
npm install @tanstack/zod-form-adapter zod
npm install @tanstack/valibot-form-adapter valibot
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>
);
}
<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>
| Cause | When |
|---|---|
onChange | After every value change |
onBlur | When field loses focus |
onSubmit | During submission |
onMount | When field mounts |
<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;
}
}}
/>
<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>
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+'),
}}
/>
const form = useForm({
defaultValues: { password: '', confirmPassword: '' },
validators: {
onChange: ({ value }) => {
if (value.password !== value.confirmPassword) {
return 'Passwords do not match';
}
return undefined;
}
}
});
<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;
}
}}
/>
<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>
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
<form.Field
name='country'
listeners={{
onChange: ({ value }) => {
// Side effect: reset dependent fields
form.setFieldValue('state', '');
form.setFieldValue('postalCode', '');
}
}}
/>
// 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>;
}
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>;
};
}
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);
import { formOptions } from '@tanstack/react-form';
const sharedOpts = formOptions({
defaultValues: { firstName: '', lastName: '' }
});
// Reuse across components
const form = useForm({
...sharedOpts,
onSubmit: async ({ value }) => {
/* ... */
}
});
// 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' }
});
}
}
// 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!
e.preventDefault() and e.stopPropagation() on form submitonBlur={field.handleBlur} for blur validation and isTouched trackingmode="array" for array fields to get array methodsundefined (not null/false) for valid validatorsasyncDebounceMs for async validators to prevent API spamisTouched before showing errors for better UXform.Subscribe with selectors to minimize re-rendersformOptions for shared configuration across componentsonChangeListenTo for cross-field validation dependenciese.preventDefault() on form submit (causes page reload)onBlur to inputs (breaks blur validation and isTouched)null or false instead of undefined for valid fieldsmode="array" incorrectly (only needed on the array field itself, not sub-fields)asyncDebounceMs with async validators (fires on every keystroke)