TanStack Form
Wire Luke UI fields to TanStack Form with form.Field.
TanStack Form owns the form state, and Luke UI renders the controls.
Initialise the form
Call useForm with defaultValues and a submit handler. revalidateLogic holds validation back
until the first submit, then revalidates each field as it changes.
const schema = z.object({
email: z.email('Enter an email address in the form you@example.com.'),
name: z.string().min(1, 'Enter your name.'),
});
const form = useForm({
defaultValues: { email: '', name: '' },
onSubmit: ({ value }) => saveAccount(value),
validationLogic: revalidateLogic({ mode: 'submit', modeAfterSubmission: 'change' }),
validators: { onDynamic: schema, onSubmit: schema },
});TanStack Form accepts any Standard Schema validator, so Zod needs no resolver package. The form's
value types come from defaultValues, and TypeScript checks the schema against them.
Integrate components
Give form.Field a name and a children function. Read the value from field.state.value, pass
field.handleChange to onChange, and pass field.handleBlur to onBlur.
A checkbox reads its value from isSelected.
Validation
Read the message from field.state.meta.errors and pass it to errorMessage. The message marks the
field invalid.
<form.Field name="email">
{(field) => (
<TextField
errorMessage={field.state.meta.errors[0]?.message}
label="Email"
onBlur={field.handleBlur}
onChange={field.handleChange}
validationBehavior="aria"
value={field.state.value}
/>
)}
</form.Field>Set validationBehavior="aria" on every field a form.Field wraps. Read
Validation for why native behaviour blocks the submit
event before the library can run.
Focus the first invalid field
TanStack Form leaves focus where it is after a failed submission. Hold a ref to the <form> element
and search it for the first control marked aria-invalid from onSubmitInvalid.
const FOCUSABLE_SELECTOR =
'input:not([type="hidden"]), select, textarea, [tabindex]:not([tabindex="-1"])';
function focusFirstInvalidField(form: HTMLFormElement | null) {
const invalid = form?.querySelector('[aria-invalid="true"]');
if (!invalid) return;
const control = invalid.matches(FOCUSABLE_SELECTOR)
? invalid
: invalid.querySelector(FOCUSABLE_SELECTOR);
if (control instanceof HTMLElement) control.focus();
}TextField, Checkbox, and ComboboxField each mark the control itself, so the first branch
matches. The second branch covers a grouped control that marks a wrapping element instead. One ref
on the form covers every field, whatever the form grows to hold.
Reach for inputRef when you need a ref to one specific control. TextField and Checkbox render
a label, description, and error message around the control, so inputRef is what reaches the input
underneath. A primitive that renders the control itself, such as ComboboxInput, takes a plain
ref.
Submitting data
Call form.handleSubmit() from the form's onSubmit, after event.preventDefault().
<form
onSubmit={(event) => {
event.preventDefault();
void form.handleSubmit();
}}
ref={formRef}
>TanStack Form runs the schema, then calls onSubmit with the values or onSubmitInvalid with the
form API.