Skip to content

useFieldArray

Manages a dynamic array of fields within a form, providing helpers for append, remove, reorder, and other array mutations while maintaining stable React keys.

Import

ts
import { useFieldArray } from '@ereo/forms'

Signature

ts
function useFieldArray<T extends Record<string, any>, Item = unknown>(
  form: FormStoreInterface<T>,
  name: string
): ArrayFieldHelpers<Item>

Parameters

NameTypeDescription
formFormStoreInterface<T>The form store (from useForm or createFormStore)
namestringDot-path to the array field (e.g. 'items' or 'order.lineItems')

Returns

ArrayFieldHelpers

NameTypeDescription
fieldsArrayFieldItem<Item>[]Current array items with stable IDs and indices
append(value: Item) => voidAdd an item to the end
prepend(value: Item) => voidAdd an item to the beginning
insert(index: number, value: Item) => voidInsert an item at a specific index
remove(index: number) => voidRemove an item by index
swap(indexA: number, indexB: number) => voidSwap two items by index
move(from: number, to: number) => voidMove an item from one index to another
replace(index: number, value: Item) => voidReplace an item at a specific index (keeps same ID)
replaceAll(values: Item[]) => voidReplace the entire array (generates new IDs)
clone(index: number) => voidDeep-clone an item and insert the copy after it

ArrayFieldItem

ts
interface ArrayFieldItem<T> {
  id: string;    // Stable unique ID -- use as React key
  value: T;      // Current value of the item
  index: number; // Current index in the array
}

Examples

Basic String List

tsx
import { useForm, useField, useFieldArray } from '@ereo/forms'

function TagsForm() {
  const form = useForm({
    defaultValues: { tags: [''] },
    onSubmit: async (values) => console.log(values.tags),
  })

  const { fields, append, remove } = useFieldArray(form, 'tags')

  return (
    <form onSubmit={(e) => { e.preventDefault(); form.handleSubmit() }}>
      {fields.map((item) => (
        <div key={item.id}>
          <input
            value={item.value}
            onChange={(e) => form.setValue(`tags.${item.index}`, e.target.value)}
          />
          <button type="button" onClick={() => remove(item.index)}>
            Remove
          </button>
        </div>
      ))}
      <button type="button" onClick={() => append('')}>
        Add Tag
      </button>
      <button type="submit">Save</button>
    </form>
  )
}

Object Array

tsx
interface LineItem {
  product: string
  quantity: number
  price: number
}

function OrderForm() {
  const form = useForm({
    defaultValues: {
      items: [{ product: '', quantity: 1, price: 0 }] as LineItem[],
    },
    onSubmit: async (values) => {
      await submitOrder(values)
    },
  })

  const { fields, append, remove } = useFieldArray(form, 'items')

  return (
    <form onSubmit={(e) => { e.preventDefault(); form.handleSubmit() }}>
      {fields.map((item) => (
        <div key={item.id}>
          <input
            value={item.value.product}
            onChange={(e) =>
              form.setValue(`items.${item.index}.product`, e.target.value)
            }
            placeholder="Product"
          />
          <input
            type="number"
            value={item.value.quantity}
            onChange={(e) =>
              form.setValue(`items.${item.index}.quantity`, Number(e.target.value))
            }
          />
          <button type="button" onClick={() => remove(item.index)}>
            Remove
          </button>
        </div>
      ))}
      <button
        type="button"
        onClick={() => append({ product: '', quantity: 1, price: 0 })}
      >
        Add Line Item
      </button>
    </form>
  )
}

All Operations

tsx
const { fields, append, prepend, insert, remove, swap, move, replace, replaceAll, clone } = useFieldArray(form, 'items')

// Add items
append({ name: 'New item' })
prepend({ name: 'First item' })
insert(2, { name: 'Third item' })

// Remove and reorder
remove(0)
swap(0, 1)
move(2, 0) // move index 2 to index 0

// Update
replace(0, { name: 'Updated' }) // keeps same stable ID
replaceAll([{ name: 'A' }, { name: 'B' }]) // generates new IDs

// Duplicate
clone(0) // deep-clones item at index 0, inserts after it

Stable Keys

Each array item gets a stable id generated as {name}-{counter}. The ID array is maintained in a parallel useRef and kept in sync with the form value:

  • append / prepend / insert / clone generate new IDs
  • remove / swap / move rearrange existing IDs
  • replace keeps the same ID (same position, new value)
  • replaceAll generates all new IDs

Always use item.id as the React key, never the index:

tsx
// Correct
fields.map((item) => <div key={item.id}>...</div>)

// Incorrect -- causes bugs on reorder
fields.map((item, i) => <div key={i}>...</div>)

Released under the MIT License.