</> useFieldArray: UseFieldArrayProps
Custom hook for working with Field Arrays (dynamic forms). The motivation is to provide a better user experience and performance. You can watch this short video to visualize the performance enhancement.
Props
| Name | Type | Required | Description |
|---|---|---|---|
name | string | ✓ | Name of the field array. Note: Dynamic names are not supported. |
control | Object | control object provided by useForm. It's optional if you are using FormProvider. | |
shouldUnregister | boolean | Whether the Field Array will be unregistered after unmounting. | |
disabled | boolean | Since v7.79.0 Disables the entire field array. When true, fields remains populated but every entry's disabled property is set to true (see Return), all mutation methods (append, prepend, insert, remove, swap, move, update, replace) become no-ops, and the array's internal subscriptions are not set up. Useful for conditionally enabling a field array in discriminated union form shapes. | |
keyName | string = "id" | Name of the attribute with autogenerated identifier to use as the key prop. If your field objects already contain an id property (e.g. database records), it will be overwritten by this auto-generated identifier unless you set a custom keyName. This prop is no longer required and will be removed in the next major version. | |
rules | Object | Since v7.34.0 The same validation rules API as for register, which includes: required, minLength, maxLength, validate. In the case of a validation error, the root property is appended to formState.errors?.<name>?.root (e.g. formState.errors?.test?.root for a field array named test), of type FieldError. Important: This is only applicable to built-in validation. |
Props example
function FieldArray() {const { control, register } = useForm()const { fields, append, prepend, remove, swap, move, insert } = useFieldArray({control, // control props comes from useForm (optional: if you are using FormProvider)name: "test", // unique name for your Field Array})return (<>{fields.map((field, index) => (<inputkey={field.id} // important to include key with field's id{...register(`test.${index}.value`)}/>))}</>)}
Return
| Name | Type | Description |
|---|---|---|
fields | (object & { id: string })[] | An array whose entries contain the defaultValue and key for each row in your component. Since v7.80.0 When the hook-level disabled prop is set, every entry's disabled property reflects it — but this is not automatically forwarded to the registered input; you need to spread it onto the input yourself (see Rules). |
append | (obj: object | object[], focusOptions) => void | Append an input or inputs to the end of your fields and focus them. The input value will be registered during this action. Important: append data is required and cannot be partial. Since v7.0.0 focusOptions accepts { shouldFocus?: boolean = true, focusIndex?: number, focusName?: string }. |
prepend | (obj: object | object[], focusOptions) => void | Prepend an input or inputs to the start of your fields and focus them. The input value will be registered during this action. Important: prepend data is required and cannot be partial. Since v7.0.0 focusOptions accepts { shouldFocus?: boolean = true, focusIndex?: number, focusName?: string }. |
insert | (index: number, value: object | object[], focusOptions) => void | Insert an input or inputs at a particular position and focus them. Important: insert data is required and cannot be partial. Since v7.0.0 focusOptions accepts { shouldFocus?: boolean = true, focusIndex?: number, focusName?: string }. |
swap | (from: number, to: number) => void | Swap the positions of inputs. |
move | (from: number, to: number) => void | Move an input or inputs to another position. |
update | (index: number, obj: object) => void | Since v7.11.0 Update an input or inputs at a particular position; updated fields will be unmounted and remounted. If this is not the desired behavior, please use the setValue API instead. Important: update data is required and cannot be partial. |
replace | (obj: object[]) => void | Since v7.15.0 Replace the entire field array's values. |
remove | (index?: number | number[]) => void | Remove an input or inputs at a particular position, or remove all if no index is provided. |
Rules
-
useFieldArrayautomatically generates a unique identifier namedidwhich is used for thekeyprop. For more information on why this is required: https://react.dev/learn/rendering-listsThe
field.id(and notindex) must be added as the component key to prevent re-renders breaking the fields:// ✅ correct:{fields.map((field, index) => <input key={field.id} ... />)}// ❌ incorrect:{fields.map((field, index) => <input key={index} ... />)} -
It's recommended to not stack actions one after another.
// ❌ avoid stacking actions in the same handleronClick={() => {append({ test: 'test' });remove(0);}}// ✅ Better solution: the remove action happens after the second renderuseEffect(() => {remove(0);}, [remove])onClick={() => {append({ test: 'test' });}} -
Each
useFieldArrayis unique and has its own state update, which means you should not have multipleuseFieldArrayinstances with the samename. -
Each input name needs to be unique. If you need to build a checkbox or radio button with the same name, use it with
useControllerorController. -
Does not support flat field arrays — each entry in a field array must be an object, not a primitive.
{ test: [{ value: 'a' }] }✅ vs{ test: ['a', 'b'] }❌. -
shouldUnregister: trueis not supported. Field array relies on inputs being mounted and unmounted to manage its internal state — enablingshouldUnregistercauses newly added fields to be unregistered on re-render, so their values are lost. Avoid combininguseFieldArraywithshouldUnregister: true. -
There is no per-entry
disabledoption — thedisabledflag on each object infields(see Return) simply mirrors the hook-leveldisabledprop, applied uniformly to every entry. It is not read from data you pass toappend/prepend/insert, and it is not automatically forwarded to the registered input — you still need to wire it up yourself:const { fields, append } = useFieldArray({control,name: "test",disabled: true,}){fields.map((field, index) => (<inputkey={field.id}disabled={field.disabled}{...register(`test.${index}.value`)}/>))} -
When you append, prepend, insert and update the field array, the object cannot be an empty object
{}; rather, you need to supply all your inputs'defaultValues.append() // ❌append({}) // ❌append({ firstName: "bill", lastName: "luo" }) // ✅
TypeScript
-
When registering an input
name, you will have to cast them asconst:<input key={field.id} {...register(`test.${index}.test` as const)} /> -
Circular references are not supported. Refer to this GitHub issue for more details.
-
For nested field arrays, you will have to cast the field array by its name:
const { fields } = useFieldArray({ name: `test.${index}.keyValue` as 'test.0.keyValue' });
Examples
import { useForm, useFieldArray } from "react-hook-form"function App() {const { register, control, handleSubmit, reset, trigger, setError } = useForm({// defaultValues: {}; you can populate the fields by this attribute})const { fields, append, remove } = useFieldArray({control,name: "test",})return (<form onSubmit={handleSubmit((data) => console.log(data))}><ul>{fields.map((item, index) => (<li key={item.id}><input {...register(`test.${index}.firstName`)} /><Controllerrender={({ field }) => <input {...field} />}name={`test.${index}.lastName`}control={control}/><button type="button" onClick={() => remove(index)}>Delete</button></li>))}</ul><buttontype="button"onClick={() => append({ firstName: "bill", lastName: "luo" })}>append</button><input type="submit" /></form>)}
Video
Tips
Custom Register
You can also register inputs at Controller without the actual input. This makes useFieldArray quick and flexible to use with complex data structures or the the actual data is not stored inside an input.
import { useForm, useFieldArray, Controller, useWatch } from "react-hook-form"const ConditionalInput = ({ control, index, field }) => {const value = useWatch({name: "test",control,})return (<Controllercontrol={control}name={`test.${index}.firstName`}render={({ field }) =>value?.[index]?.checkbox === "on" ? <input {...field} /> : null}/>)}function App() {const { control, register } = useForm()const { fields, append, prepend } = useFieldArray({control,name: "test",})return (<form>{fields.map((field, index) => (<ConditionalInput key={field.id} {...{ control, index, field }} />))}</form>)}
Controlled Field Array
There will be cases where you want to control the entire field array, which means each onChange reflects on the fields object.
import { useForm, useFieldArray } from "react-hook-form"export default function App() {const { register, handleSubmit, control, watch } = useForm()const { fields, append } = useFieldArray({control,name: "fieldArray",})const watchFieldArray = watch("fieldArray")const controlledFields = fields.map((field, index) => {return {...field,...watchFieldArray[index],}})return (<form>{controlledFields.map((field, index) => {return (<inputkey={field.id}{...register(`fieldArray.${index}.name` as const)}/>)})}</form>)}
Thank you for your support
If you find React Hook Form to be useful in your project, please consider starring and supporting it.