unity-tanstack-form
Load when building or migrating Unity forms. Use useTanstackUnityForm with schema validation and prefer Tanstack Form field APIs for composed fields, custom layouts, and validation behavior.
This skill targets the current workspace Unity v3 API. Legacy examples are migration context.
Build a Unity v3 form with useTanstackUnityForm + Zod. The legacy RHF integration (useUnityForm) was removed in v3; the wrong example below is historical migration context, not a supported API.
Setup
import { Button } from '@payfit/unity-components'
import { useTanstackUnityForm } from '@payfit/unity-components/forms'
import { z } from 'zod'
const schema = z.object({
email: z.email({ message: 'Invalid email' }),
password: z.string().min(8, { message: 'Password too short' }),
})
export function SignInForm() {
const form = useTanstackUnityForm({
defaultValues: { email: '', password: '' },
validators: { onBlur: schema },
onSubmit: async ({ value }) => {
await signIn(value)
},
})
return (
<form.AppForm>
<form.Form className="uy:space-y-200">
<form.AppField name="email">
{field => <field.TextField label="Email" type="email" />}
</form.AppField>
<form.AppField name="password">
{field => <field.PasswordField label="Password" />}
</form.AppField>
<Button variant="primary" type="submit">
Sign in
</Button>
</form.Form>
</form.AppForm>
)
}Wrapping order is load-bearing: form.AppForm provides the form context, form.Form renders the <form> element and wires handleSubmit, form.AppField provides field context, and the render-prop field carries every bound field component (TextField, PasswordField, SelectField, …) plus the Atomic parts (Field, FieldLabel, TextInput, FieldFeedbackText, FieldHelperText, FieldRawContextualLink).
Core Patterns
- Prefer composed field.*Field components for ordinary fields.
- Use the atomic field parts only when custom layout or control composition is required.
- Default validation to blur; use dynamic validation for fields that need blur-then-change behavior.
- Narrow every form.Subscribe with a selector.
Read references/patterns.md for complete examples.
Common Mistakes
CRITICAL Import useForm (legacy RHF) instead of useTanstackUnityForm
Wrong:
import { useUnityForm } from '@payfit/unity-components'
const { methods, Form, FormField } = useUnityForm(schema)Correct:
import { useTanstackUnityForm } from '@payfit/unity-components/forms'
const form = useTanstackUnityForm({ validators: { onBlur: schema } })The legacy useUnityForm hook and RHF field wrappers were removed in v3. Replace them with useTanstackUnityForm and its bound field.* components; do not mix RHF Controller with Tanstack field components.
Source: libs/shared/unity/components/src/docs/guides/migration/Migrating from 2.x to 3.x.mdx (React Hook Form removal)
CRITICAL Omit form.AppForm or form.AppField wrapping
Wrong:
<form.Form>
<form.AppField name="email">
{field => <field.TextField label="Email" />}
</form.AppField>
</form.Form>Correct:
<form.AppForm>
<form.Form>
<form.AppField name="email">
{field => <field.TextField label="Email" />}
</form.AppField>
</form.Form>
</form.AppForm>useFormContext() and useFieldContext() throw without their providers; Tanstack field components silently break (cannot read property of undefined).
Source: src/modules/tanstack-form/components/form/TanstackForm.tsx; src/modules/tanstack-form/components/form-field/TanstackFormField.tsx (useFormContext/useFieldContext)
HIGH Subscribe to whole form state instead of a selector
Wrong:
<form.Subscribe>
{state => <div>Length: {state.values.password.length}</div>}
</form.Subscribe>Correct:
<form.Subscribe selector={s => s.values.password}>
{password => <div>Length: {password.length}</div>}
</form.Subscribe>A selector-less subscription re-renders the children on every keystroke anywhere in the form; pass a narrowing selector to scope to the slice you need.
Source: src/modules/tanstack-form/hooks/use-tanstack-form.stories.tsx (StateIntegration story)
MEDIUM Pick the wrong validation timing
Wrong:
useTanstackUnityForm({ validators: { onChange: schema } })Correct:
useTanstackUnityForm({ validators: { onBlur: schema } })
// or with revalidation:
// validationLogic: fieldRevalidateLogic({ fields: ['password'], whenDirty: 'change' })onChange fires on every keystroke (jarring) and onSubmit waits until submit (errors arrive too late); onBlur is the usual default, with fieldRevalidateLogic reserved for "blur until first error, then change".
Source: src/modules/tanstack-form/hooks/use-tanstack-form.stories.tsx; src/modules/tanstack-form/utils/field-revalidate-logic.ts
HIGH Mix Composed and Atomic APIs in one field (or reach for Atomic by default)
Wrong:
// Double-wrapping:
<form.AppField name="email">
{field => (
<field.Field>
<field.FieldLabel>Email</field.FieldLabel>
<field.TextField label="Email" />
</field.Field>
)}
</form.AppField>
// Or reaching for Atomic with no customization reason:
<form.AppField name="name">
{field => (
<field.Field>
<field.FieldLabel>Name</field.FieldLabel>
<field.TextInput />
<field.FieldFeedbackText />
</field.Field>
)}
</form.AppField>Correct:
// Default (Composed):
<form.AppField name="name">
{field => <field.TextField label="Name" />}
</form.AppField>
// Atomic only when customizing layout/parts:
<form.AppField name="email">
{field => (
<field.Field>
<field.FieldLabel>Email</field.FieldLabel>
<CustomInline>
<field.TextInput />
<ExtraSlot />
</CustomInline>
<field.FieldFeedbackText />
</field.Field>
)}
</form.AppField>field.TextField is the Composed API and already includes label/input/feedback; wrapping it in field.Field + field.FieldLabel double-wraps and breaks layout + a11y. Default to Composed; reach for Atomic only when you must customize the field's layout or swap a part — never as the standard pattern.
Source: TanstackTextField.tsx; TanstackFormField.tsx and its parts
References
- Bound field components — full inventory of field.* Composed components and their underlying base components.
- Schema adapters — StandardSchemaAdapter, ZodV3SchemaAdapter, and ZodV4SchemaAdapter.
See also
- unity-migrate-from-midnight — forms migrated off Midnight typically came with React Hook Form; that skill explains the Tanstack-only replacement path.
- unity-layout — compose form layouts with Flex and Grid.
- unity-themes — use uy:* spacing and responsive utilities.
- unity-navigation — when a form posts via a route action or links to a sibling step, use the router-aware Link from @payfit/unity-components/integrations/tanstack-router.