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 rowsprepend
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 rowremove
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, notitems[0].qty) plusancestors. - Functions that take an
indexuse the row's current position, the index inrowIds.