TanStack Form fields can hold arrays just like any other value. The important distinction is whether the array is the field value itself or whether you're rendering fields for the items inside it.
If one control reads and replaces the whole array, use a regular field. If you're rendering fields for individual items and need to add, remove, or reorder them, use an array field.
This guide covers both cases and how to choose between them.
Use a regular field when one control reads and replaces the complete array. A multi-select, tag picker, or checkbox group can pass its next array to handleChange like any other field value:
<form.Field name="selectedRoleIds">
{(field) => (
<RolePicker
value={field.value}
onValueChange={(roleIds) => field.handleChange(roleIds)}
/>
)}
</form.Field>If you need to edit fields inside each array item, the previous approach has two problems.
This is where form.ArrayField comes in. It ensures that each item in the array is rendered independently, while still subscribing to item changes like swapping, removing or pushing new values.
<form.ArrayField name="people">
{(array) => (
<ul>
{array.value.map((person, index) => (
<li key={person.id}>
<form.Field name={`people[${index}].name`}>
{(field) => <TextInput field={field} />}
</form.Field>
</li>
))}
</ul>
)}
</form.ArrayField>While the nested fields use the index in their name, use your adapter's identity mechanism to keep each object tied to the same rendered item.
array.value.map((person) => <li key={person.id}>Item fields</li>)Use the array field's mutation methods for structural changes. You can perform the same mutations from the form API by passing the field name first.
// Append an item.
field.pushValue(value)
// Insert an item at an index.
field.insertValue(index, value)
// Remove the item at an index.
field.removeValue(index)
// Exchange two items.
field.swapValues(indexA, indexB)
// Move an item from one index to another.
field.moveValue(fromIndex, toIndex)
// Keep the items accepted by a predicate.
field.filterValues((value, i) => isEnabled(value))
// Remove every item.
field.clearValues()// Append an item.
form.pushFieldValue('items', value)
// Insert an item at an index.
form.insertFieldValue('items', index, value)
// Remove the item at an index.
form.removeFieldValue('items', index)
// Exchange two items.
form.swapFieldValues('items', indexA, indexB)
// Move an item from one index to another.
form.moveFieldValue('items', fromIndex, toIndex)
// Keep the items accepted by a predicate.
form.filterFieldValues('items', (value, i) => isEnabled(value))
// Remove every item.
form.clearFieldValues('items')You could make these changes yourself with handleChange, but these helpers also take care of the registered fields for each item. When an item moves, its field state moves with it. When an item is removed, its field state is removed too.
A successful array mutation marks the array field as touched and dirty and causes validation by default.