Luke UI
GitHub repository

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.

TanStack Form — Text fields

A checkbox reads its value from isSelected.

TanStack Form — Checkbox

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.

Continue learning