.md

Field-array mutations

Seven shape-changing helpers, typed against every array path in your schema. Stable per-item identity across moves, removes, and swaps.

Category
Return methods
Helpers
append · prepend · insert · remove · swap · move · replace
Type
typed against ArrayPath<Form>
Stable keys
Yes, per-item identity preserved

Use the row arrows and per-row × button to move and remove items; the buttons below dispatch every helper against the array. Watch the readout: the order, count, and contents reflect each call. Stable keys mean per-item validation state, dirty bits, and DOM focus survive shape changes; reordering doesn't reset what the user typed.

Field Arrays Demo Open in playground
[
  "Warm-up",
  "Sprint",
  "Cooldown"
]

The seven helpers

Each helper is typed against the form's ArrayPath<Form> set; TypeScript autocompletes only the paths that actually point at an array. The value-shape generic narrows on the inferred element type.

HelperWhat it does
append(path, value)Adds one item at the end.
prepend(path, value)Adds one item to the front; shifts the rest right.
insert(path, index, value)Inserts one item at index; shifts the rest right.
remove(path, index)Removes the item at index; shifts the rest left.
swap(path, a, b)Exchanges the items at the two indices.
move(path, from, to)Removes from from and re-inserts at to in one step.
replace(path, index, value)Overwrites the item at index without changing length.

Reading the path

const form = useForm({
  schema: z.object({
    checkpoints: z.array(z.string()),
  }),
})

form.append('checkpoints', 'New checkpoint')
form.remove('checkpoints', 2)

The path string autocompletes to every array path in the schema. Nested arrays work the same way: register('teams.0.players') for an inner array, and the helpers take the same form.

Every array counts, including the ones that may be absent at runtime: z.array(...).optional(), .default([]), .nullable(), and an array that lives inside a z.discriminatedUnion variant. Adding to one that is absent creates it, reordering or removing while it is absent is a no-op, and form.list reads it as empty.

Stable per-item identity

Every helper preserves the existing items' reactive identity, and an item's full state travels with it to its new index:

  • append leaves indices 0..n unchanged.
  • prepend, insert, swap, and move carry the moved item's value, its original baseline, its dirty and touched state, any error you set on it, its blank display, and its bound DOM element to the new index. Nothing bleeds onto the item that shifts into the vacated slot.
  • remove drops the removed item's state; replace starts the incoming item fresh.

Practical consequence: a user typing in row 3, hitting "Move up", finishes typing in row 2 without losing focus or having the entered text reset. A row already marked dirty stays dirty at its new index, and the row that shifts into the old slot keeps its own clean state. Attaform tracks the item, not the slot.

Because each item carries its own baseline, Attaform still reads a structural change as a change: a reorder, insert, or removal leaves form.meta.dirty true even when every surviving item matches its own baseline.

Validation per item

Per-item validation tracks the item, not the slot. An error you set with form.setErrors on checkpoints.0 follows the item through a move(0, 4) to checkpoints.4. Schema verdicts recompute from the live value after each shape change, so a still-invalid item shows its error at its new index, and a removed item's verdict clears at once instead of lingering on whatever shifts into the slot.

For array-level refinements (z.array(...).min(3) or .refine(arr => arr.length > 0)), the error lands at the array path itself, not at any slot. Read it with form.errors.checkpoints[''] or form.fields('checkpoints').firstOwnError, both of which mean the array's own errors. The looser form.errors('checkpoints') works too, but it is the flat aggregate for the whole subtree, so a bad element's message arrives alongside the array's.

Reset behavior

reset() restores the array's default: same length, same item values, every item back to its pre-edit state. clear('checkpoints') writes []. No special "reset the array's helpers", same pipeline as the rest of the writes.

Where to next