@payfit/unity-components
Version:
239 lines (185 loc) • 8.07 kB
Markdown
---
name: unity-tanstack-form
description: >
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.
metadata:
type: core
library: '/unity-components'
library_version: '3.x'
sources:
- 'PayFit/hr-apps:libs/shared/unity/components/src/modules/tanstack-form/hooks/use-tanstack-form.tsx'
- 'PayFit/hr-apps:libs/shared/unity/components/src/docs/guides/migration/Migrating from 2.x to 3.x.mdx'
- 'PayFit/hr-apps:libs/shared/unity/components/src/modules/tanstack-form/components/form/TanstackForm.tsx'
- 'PayFit/hr-apps:libs/shared/unity/components/src/modules/tanstack-form/components/form-field/TanstackFormField.tsx'
- 'PayFit/hr-apps:libs/shared/unity/components/src/modules/tanstack-form/adapters/zodAdapter.ts'
- 'PayFit/hr-apps:libs/shared/unity/components/src/modules/tanstack-form/utils/field-revalidate-logic.ts'
- 'PayFit/hr-apps:libs/shared/unity/components/src/modules/tanstack-form/docs/concepts/Form Architecture Overview.mdx'
---
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
```tsx
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](references/patterns.md) for complete examples.
## Common Mistakes
### CRITICAL Import useForm (legacy RHF) instead of useTanstackUnityForm
Wrong:
```tsx
import { useUnityForm } from '@payfit/unity-components'
const { methods, Form, FormField } = useUnityForm(schema)
```
Correct:
```tsx
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:
```tsx
<form.Form>
<form.AppField name="email">
{field => <field.TextField label="Email" />}
</form.AppField>
</form.Form>
```
Correct:
```tsx
<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:
```tsx
<form.Subscribe>
{state => <div>Length: {state.values.password.length}</div>}
</form.Subscribe>
```
Correct:
```tsx
<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:
```tsx
useTanstackUnityForm({ validators: { onChange: schema } })
```
Correct:
```tsx
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:
```tsx
// 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:
```tsx
// 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](references/bound-field-components.md) — full inventory of `field.*` Composed components and their underlying base components.
- [Schema adapters](references/schema-adapters.md) — 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 `/unity-components/integrations/tanstack-router`.