Loading...
Loading...
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety
npx skill4agent add agents-inc/skills web-forms-tanstack-formQuick Guide: UsewithuseFormand typed generics. Render fields withdefaultValuesusing the render-propform.Fieldpattern. Validation lives in thechildrenprop on both form and field level — usevalidators,onChange,onBlur(sync) and theironSubmitvariants. UseAsyncfor dynamic field lists withmode="array"/pushValue. UseremoveValuefor cross-field validation. For app-wide consistency, create a sharedonChangeListenToviauseAppForm. Always providecreateFormHook— TanStack Form infers types from them.defaultValues
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,, named constants)import type
defaultValuesuseFormform.FieldchildrenregisterControllervalidatorsrulesfield.state.meta.errors.map()form.handleSubmit()onSubmite.preventDefault()defaultValuesdefaultValuesdefaultValuesonChangeonBluronSubmitcreateFormHookuseFormform.FieldchildrenstatehandleChangehandleBlurimport { useForm } from "@tanstack/react-form";
const form = useForm({
defaultValues: { name: "", email: "" },
onSubmit: async ({ value }) => {
await submitToApi(value);
},
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<form.Field
name="email"
children={(field) => (
<input
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
</form>
);registerControllerreffield.handleChangefield.state.valuevalidatorsundefinedonChangeAsynconBlurAsynconSubmitAsync<form.Field
name="age"
validators={{
onChange: ({ value }) => (value < 13 ? "Must be 13 or older" : undefined),
onBlurAsync: async ({ value }) => {
const exists = await checkAge(value);
return exists ? undefined : "Age not valid on server";
},
}}
children={(field) => (
<div>
<input
type="number"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{field.state.meta.errors.map((err) => (
<em key={err} role="alert">
{err}
</em>
))}
</div>
)}
/>onBluronBlurAsynconChangeonChangeAsynconChangeListenTo<form.Field
name="confirm_password"
validators={{
onChangeListenTo: ["password"],
onChange: ({ value, fieldApi }) => {
if (value !== fieldApi.form.getFieldValue("password")) {
return "Passwords do not match";
}
return undefined;
},
}}
children={(field) => (/* ... */)}
/>onChangeListenTopasswordconfirm_passwordmode="array"form.FieldpushValueremoveValueswapValuesmoveValueinsertValue<form.Field
name="hobbies"
mode="array"
children={(hobbiesField) => (
<div>
{hobbiesField.state.value.map((_, i) => (
<div key={i}>
<form.Field
name={`hobbies[${i}].name`}
children={(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
/>
<button type="button" onClick={() => hobbiesField.removeValue(i)}>
Remove
</button>
</div>
))}
<button
type="button"
onClick={() => hobbiesField.pushValue({ name: "" })}
>
Add hobby
</button>
</div>
)}
/>pushValueuseFormonSubmitAsyncconst form = useForm({
defaultValues: { username: "", age: 0 },
validators: {
onSubmitAsync: async ({ value }) => {
const errors = await validateOnServer(value);
if (errors) {
return {
form: "Submission failed",
fields: {
username: errors.username,
age: errors.age,
},
};
}
return null;
},
},
});{ form?: string, fields: Record<string, string> }formfieldsnullcreateFormHookimport { createFormHookContexts, createFormHook } from "@tanstack/react-form";
export const { fieldContext, formContext, useFieldContext } =
createFormHookContexts();
export const { useAppForm, withForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: {
TextField: TextFieldComponent,
SelectField: SelectFieldComponent,
},
formComponents: {
SubmitButton: SubmitButtonComponent,
},
});useAppFormuseFormfieldComponentsformComponentsform.AppFieldform.AppFormlistenersform.Field<form.Field
name="country"
listeners={{
onChange: ({ value }) => {
form.setFieldValue("province", "");
},
}}
children={(field) => (/* ... */)}
/>onChangeonBluronMountonSubmitform.Subscribeselector<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit || isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit"}
</button>
)}
/>form.Subscribeform.stateregisterControllerform.FieldchildrendefaultValuesuseFormundefinedform.handleSubmit()e.preventDefault()form.stateform.SubscribeuseStoreonChangeonChangeAsynconBlurAsyncpushValueonChangeListenToform.handleSubmit()handleSubmitonSubmitfield.state.meta.errors===.map().lengthonChangeonChangeAsynconSubmitAsync{ fields: { fieldName: "error" } }field.state.meta.isTouchedtruehandleBlurhandleChangename={}items.${i}.nameform.SubscribeselectorcreateFormHookform.AppFieldform.AppFormform.FieldAll code must follow project conventions in CLAUDE.md
defaultValuesuseFormform.FieldchildrenregisterControllervalidatorsrulesfield.state.meta.errors.map()form.handleSubmit()onSubmite.preventDefault()