Array Fields
Forms often need to handle dynamic lists — phone numbers, addresses, team members, tags. TanStack Form handles this with array fields, giving you full control over adding, removing, and validating each item.
When You Need Arrays
Any time the number of inputs isn't fixed at design time, you need an array field. A user might have one phone number or five. A project might have no team members or a dozen. Array fields let the UI grow and shrink with the data.
Setting Up an Array Field
Start by including the array in your defaultValues, then use mode="array" on the parent form.Field:
const form = useForm({
defaultValues: {
phones: [''],
},
onSubmit: async ({ value }) => console.log(value),
})
<form.Field
name="phones"
mode="array"
children={(field) => (
<div>
{field.state.value.map((_, i) => (
<form.Field
key={i}
name={`phones[${i}]`}
children={(subField) => (
<input
value={subField.state.value}
onChange={(e) => subField.handleChange(e.target.value)}
/>
)}
/>
))}
</div>
)}
/>
The outer field owns the array. Each inner form.Field owns a single index. The name phones[0], phones[1], etc. is how TanStack Form maps back to the right slot in state.
Adding Items
The field object exposes pushValue, which appends a new item to the array:
<button type="button" onClick={() => field.pushValue('')}>
Add Phone
</button>
Call this inside the outer field's children render where you have access to field.
Removing Items
field.removeValue(index) drops the item at that index and shifts everything else:
<button type="button" onClick={() => field.removeValue(i)}>
Remove
</button>
Pair this with each rendered input so users can delete any row.
Array of Objects
Real-world arrays usually hold objects, not primitives. Say you're collecting people with a name and age:
defaultValues: {
people: [{ name: '', age: 0 }],
}
Access nested fields using dot notation after the index:
<form.Field
name={`people[${i}].name`}
children={(subField) => (
<input
value={subField.state.value}
onChange={(e) => subField.handleChange(e.target.value)}
/>
)}
/>
Each property is its own independent field with its own state, validation, and touch tracking.
Complete Example
Here's a full dynamic phone number list with add and remove:
<form.Field
name="phones"
mode="array"
children={(field) => (
<div>
{field.state.value.map((_, i) => (
<form.Field
key={i}
name={`phones[${i}]`}
children={(subField) => (
<div style={{ display: 'flex', gap: '8px', marginBottom: '8px' }}>
<input
value={subField.state.value}
onChange={(e) => subField.handleChange(e.target.value)}
placeholder="Phone number"
/>
<button type="button" onClick={() => field.removeValue(i)}>
Remove
</button>
</div>
)}
/>
))}
<button type="button" onClick={() => field.pushValue('')}>
Add Phone
</button>
</div>
)}
/>
The pattern is always the same: outer field with mode="array" manages the list, inner fields manage individual items, and pushValue/removeValue handle mutations. Once you have this wired up, validation on each sub-field works exactly like it does on any other field.