Fields

The four shapes of a field entry, nested objects, and TanStack's options per field.

fields describes what a form contains, and its shape follows the data: an object in the data is an object in the config, an array is an array. Each entry under fields has one of four shapes, marked by one key:

ShapeMarkerFor
Leafwidgetone value, rendered by a widget
Objectfieldsa value with parts
Arrayitema list, one entry per item
Customrendera field you render yourself

The shapes exclude each other. An entry with a widget and fields does not compile, so an entry never half-works.

How a level renders

How Fields renders a levelFields walks a level and renders each field entry at its path, by its shape. A leaf mounts a TanStack field and hands it to its widget; an object descends into a level of its own; an array mounts a field for the list and renders one item entry per item; a custom entry mounts a field and calls its render function.CONFIGLevelfields · layoutPER NAMEField entryat its pathWIDGETLeafAppField → widgetFIELDSObjectdescends one levelITEMArrayAppField → itemsRENDERCustomAppField → renderLEVELNested leveladdress.zipPER ITEMItem entriescontacts[0].name

Fields walks a level and renders each entry at its path, the name TanStack knows the field by: email, address.zip, contacts[0].name. You never write a path in a config; formdef builds it while it descends.

Leaf

A leaf names a widget and passes it its props:

export const  = createForm({
  : .({ : .() }),
  : { : '' },
  fields: {
    email: { : 'text', : { : 'Email', : 'you@example.com' } },
  },
})
KeyMeaning
widgetThe registered widget that renders the field. It must serve the field's value type.
propsThe widget's own props, checked against that widget. Required when the widget has a prop it cannot render without.

props may also be a function of the form's values; Visibility and derived props shows how.

A widget that takes required can only receive required: false where the schema makes the key optional, so a config never claims a value the schema does not demand:

export const  = createForm({
  : .({ : .().(1), : .().() }),
  : { : '' },
  fields: {
    name: { : 'text', : { : 'Name', : true } },
    nickname: { : 'text', props: { : 'Nickname', : true } },
Type '{ label: "Nickname"; required: true; }' is not assignable to type '{ readonly label: "Nickname"; readonly required: true; } & ((values: { name: string; nickname?: string | undefined; }) => { label: string; required?: false; }) & { readonly label: "Nickname"; readonly required: true; }'. Type '{ label: "Nickname"; required: true; }' is not assignable to type '(values: { name: string; nickname?: string | undefined; }) => { label: string; required?: false; }'. Type '{ label: "Nickname"; required: true; }' provides no match for the signature '(values: { name: string; nickname?: string | undefined; }): { label: string; required?: false; }'.
}, })

Object

An object entry describes the parts of a value with fields of its own:

const  = .({
  : .(),
  : .({ : .(), : .() }),
})

export const  = createForm({
  ,
  : { : '', : { : '', : '' } },
  fields: {
    name: { : 'text', : { : 'Name' } },
    address: {
      : {
        zip: { : 'text', : { : 'ZIP code' } },
        city: { : 'text', : { : 'City' } },
      },
    },
  },
})

The inputs land at address.zip and address.city. An object renders no markup of its own: its fields appear where the object sits. To draw a box around them, use a section in the layout. Each object is a level and may carry its own layout, which names the keys of that level only.

An object entry may carry TanStack's field options too, for a rule over the whole object. formdef then mounts a field at the object's path, so the rule runs; without options it mounts none.

export const  = createForm({
  ,
  : { : { : '', : '' } },
  fields: {
    address: {
      : {
        : ({  }) =>
          . !== '' && . === '' ? 'A ZIP code needs a city' : ,
        : ['address.zip', 'address.city'],
      },
      : {
        zip: { : 'text', : { : 'ZIP code' } },
        city: { : 'text', : { : 'City' } },
      },
    },
  },
})

onChangeListenTo makes the rule run when a part changes: TanStack validates the field whose value was set, and setting address.zip alone does not run a rule on address.

Leaving keys out

A key of the data that fields does not describe is not rendered. It keeps its value, the schema still validates it, and it is submitted, which suits ids and values the server manages. A key the data does not have is a compile error.

One widget for a whole array

A list that is one control, such as tags or a multi-select, is a leaf whose widget serves the array type, FieldOf<string[]>:

export const  = createForm({
  : .({ : .(.()) }),
  : { tags: [] },
  fields: { tags: { : 'tags', : { : 'Tags' } } },
})

An array whose items each get their own fields is an array entry; Arrays covers it.

Custom

A custom entry hands you TanStack's field, typed with the field's value, and renders what you return:

export const  = createForm({
  : .({ : .(), : .() }),
  : { : '', : '' },
  fields: {
    name: { : 'text', : { : 'Name' } },
    avatar: {
      : () => (
        < ={.state.} ={() => .()} />
      ),
    },
  },
})

Use it for a control that does not fit the widget contract, or for one form only. It is what you would write against TanStack directly, inside a config.

TanStack's options per field

Every entry, whatever its shape, takes TanStack's field options and passes them on unchanged: validators, listeners, asyncDebounceMs, asyncAlways, defaultValue, defaultMeta, disableErrorFlat and mode. An array entry takes all of them except mode: formdef renders it in TanStack's array mode, so the list renders again when items are added, removed or moved, not when one of them changes.

export const  = createForm({
  : .({ : .().(3) }),
  : { : '' },
  fields: {
    username: {
      : 'text',
      : { : 'Username' },
      : 300,
      : {
        : async ({  }) =>
          (await ()) ? 'This username is taken' : ,
      },
    },
  },
})

Validators and listeners on an entry are typed against that field's value, and they run next to the schema.