Lesson 10

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.