tanstack-form
SkillWeb & browsingHeadless, performant, and type-safe form state management for React. Use before any form work in the renderer (drafts, field validation, submit state).
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the tanstack-form skill
What this skill tells your AI
The instructions your AI receives, as published by recomposesh/recompose in .claude/skills/tanstack-form/SKILL.md and read by ahel’s review.
Overview
TanStack Form manages form state: values, per-field validation, submit lifecycle, and derived flags like canSubmit and isSubmitting. It is headless, so the design system's own controls render the fields, and it is type-safe end to end from defaultValues to onSubmit.
Package: @tanstack/react-form
Current version: v1
Installation
pnpm add @tanstack/react-form --save-exact
Core anatomy
One useForm per draft. Fields render through form.Field, submit state renders through form.Subscribe, and the native <form> wires handleSubmit.
import { useForm } from '@tanstack/react-form';
function Draft() {
const form = useForm({
defaultValues: { name: '' },
onSubmit: ({ value }) => {
store(value);
},
});
return (
<form
onSubmit={(event) => {
event.preventDefault();
void form.handleSubmit();
}}
>
<form.Field name="name">
{(field) => (
<input
value={field.state.value}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
/>
)}
</form.Field>
<form.Subscribe selector={(state) => [state.canSubmit, state.isSubmitting]}>
{([canSubmit, isSubmitting]) => (
<button disabled={!canSubmit || isSubmitting} type="submit">
Save
</button>
)}
</form.Subscribe>
</form>
);
}
Validation
Validators live on the field, keyed by when they run. A validator answers undefined for a value that stands, or the refusal sentence.
<form.Field
name="name"
validators={{
onChange: ({ value }) => (value.trim() === '' ? 'A gateway needs a name.' : undefined),
}}
>
{(field) => (
<>
<input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />
{field.state.meta.isValid ? null : <p role="alert">{field.state.meta.errors.join(' ')}</p>}
</>
)}
</form.Field>
Standard Schema validators (zod schemas from @recompose/contracts) drop in directly:
validators={{ onChange: pastedKeySchema }}
Server refusals
A refusal the main process answers after submit belongs to the field it refuses. Map it through an onSubmitAsync form validator that returns { fields: { name: 'That name is held.' } }, or keep the mutation's own refusal line outside the form when the refusal concerns the whole draft.
Subscribing without re-rendering the world
form.Subscribe re-renders only its children when the selected slice changes. useStore(form.store, selector) does the same for values a sibling needs (a live preview, a shape warning).
const port = useStore(form.store, (state) => state.values.port);
House conventions
- One
useFormper sheet draft; the form never outlives its surface, so a dismissal forgets the draft. - Fields render the design system's own rows (
FieldBoxRow); TanStack Form stays invisible to the reading. - Submit acts read
canSubmitand the mutation's own pending flag; a mutation refusal that concerns one field lands under that field, and a draft-wide refusal keeps itsrole="alert"line. - Behavior specs stay on roles and accessible names; nothing asserts on form internals.
Common pitfalls
form.handleSubmit()returns a promise;voidit in the DOM handler afterpreventDefault.canSubmitstandstrueon a pristine form; a draft that must not submit empty needs anonChange/onMountvalidator or a value-driven disabled flag.- Do not read
form.stateduring render for reactive values; subscribe throughform.SubscribeoruseStore. - Keep
defaultValuesreferentially stable; inline object literals re-created per render reset nothing but cost reconciliation.
Signals
- GitHub stars
- 27
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
tanstack-form-recomposesh- Source
- github.com/recomposesh/recompose