원클릭으로
tanstack-form
Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, Lit, and Svelte.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, Lit, and Svelte.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
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.
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
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.
Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling
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.
| 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)