Wit Form

useFieldArray

useFieldArray manages a list of rows that share the same fields: line items, guests, table rows. It tracks which rows exist and in what order, and gives you functions to add, insert, move and remove them. The inputs inside each row are still regular useField fields.

import { useFieldArray } from 'wit-form';

const array = useFieldArray(options);
const { fieldArrayProps, append, remove } = useFieldArray({
  name: 'items',
  fieldNames: ['product', 'qty'],
});

return fieldArrayProps.rowIds.map((rowId, index) => (
  <ItemRow key={rowId} rowId={rowId} onRemove={() => remove(index)} />
));

Read the Field Arrays guide for a complete walkthrough.

Options

name

Type: string · Required

Where the list lives in the form values. items gives { items: [...] }. For a list inside another list's row, use the plain name, like lessons, together with ancestors.

fieldNames

Type: (string | { name: string; type: 'field' } | { name: string; type: 'field-array'; fieldNames: [...] })[] · Required

The fields in each row. Wit Form uses this list to build each row's object in the submitted values, and to fill rows from initial values.

fieldNames: ['product', 'qty', 'price'];

Strings and { name, type: 'field' } mean the same thing. The type: 'field-array' form declares a nested list, so new rows (from append, insert, update or setFieldArrayValue) can include their nested rows. See Nested field arrays.

ancestors

Type: { name: string; rowId: number }[] · Default: []

Only for nested lists. The row (or rows) this list is inside, outermost first. Memoize it with useMemo.

// The lessons list inside one section row
useFieldArray({
  name: 'lessons',
  fieldNames: ['title'],
  ancestors: [{ name: 'sections', rowId: sectionRowId }],
});

validate

Type: (rows: any[]) => string | null | undefined | Promise<...> · Default: none

Validates the list as a whole, like "Add at least one item". Receives every row as a plain object and returns an error message or null. It can be async. It's checked whenever the rows change and on submit. The result is returned as error.

Define it outside your component or memoize it. When validate is set, the component calling useFieldArray re-renders on every change inside the list, because the validator needs all values. Skip it on very large lists. See Performance.

schema

Type: StandardSchemaV1 · Default: none

A schema for the list of rows, from any Standard Schema library. Runs before validate, like z.array(z.object({ ... })).min(1, 'Add an item').

defaultValue

Type: object[] · Default: none

The rows to start with when initialValues has none for this list. Most useful for nested lists, where it gives each new parent row its first child rows.

skipUnregister

Type: boolean · Default: false

Keep the rows when this component unmounts. Without it, the rows are discarded on unmount.

Returns

fieldArrayProps.rowIds

Type: number[]

The id of each row, in display order. Render one row per id. Use the id as the React key and in each field's ancestors. Ids stay the same when other rows are added or removed. The index of an id in this array is the row's position.

append

Type: (...rows: object[]) => void

Adds rows at the end. With no arguments, adds one empty row. Fields you leave out use their defaultValue.

append(); // one empty row
append({ product: 'Pen', qty: 1 }); // one row with values
append(rowA, rowB); // two rows

prepend

Type: (...rows: object[]) => void

Adds rows at the start. With no arguments, adds one empty row.

insert

Type: (index: number, ...rows: object[]) => void

Inserts rows at index, pushing later rows down. With no rows, inserts one empty row.

insert(0, { qty: 1 }); // at the top
insert(index + 1, getFieldArrayValue()[index]); // duplicate a row

remove

Type: (index: number | number[]) => void

Removes the row at index, or the rows at several indexes:

remove(2);
remove([0, 3, 4]);

swap

Type: (indexA: number, indexB: number) => void

Swaps two rows. Only the order changes: both rows keep their ids, values, errors and touched state.

move

Type: (from: number, to: number) => void

Moves the row at from to to, shifting the rows in between. Use it after drag and drop. Like swap, rows keep their ids and state.

update

Type: (index: number, row: object, extraInfo?: object) => void

Replaces the values of the row at index. The row keeps its id, so its components don't remount. Nested lists that the row object leaves out keep their rows.

clear

Type: (index: number) => void

Resets the fields of the row at index, back to their defaultValue if they have one, without removing the row.

removeAll

Type: () => void

Removes every row.

getFieldArrayValue

Type: () => object[]

Returns the current rows as plain objects. It reads them once and doesn't subscribe, so use it in event handlers. To show rows live, use useFieldArrayColumnWatch.

setFieldArrayValue

Type: (rows: object[]) => void

Replaces all rows. Rows are reused where possible and extra ones are added or removed.

replace

Type: (rows: object[]) => void

The same as setFieldArrayValue.

validateData

Type: () => { errors: IFieldError[]; isValid: boolean }

Validates the list and the fields in its rows right now, shows their errors, and returns the result. Only sync validators are included. Async validators start, and their errors show when they finish.

validateDataAsync

Type: () => Promise<{ errors: IFieldError[]; isValid: boolean }>

Like validateData, but waits for async validators.

error

Type: string | null | undefined

The list's error: from validate or schema, from the form schema (an issue at the list's path), or from setError. Unlike field errors, it doesn't wait for a touch: it shows as soon as it's set.

isValidating

Type: boolean

true while an async validate runs.

Notes

  • Fields inside rows use plain names (qty, not items[0].qty) plus ancestors.
  • Functions that take an index use the row's current position, the index in rowIds.

On this page