</> useFieldArray: UseFieldArrayProps
Custom hook for working with Field Arrays (dynamic form). The motivation is to provide 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 Field Array will be unregistered after unmount. | |
disabled | boolean | Disables the entire field array. When true, fields initializes as an empty array, all mutation methods (append, prepend, insert, remove, swap, move, update, replace) become no-ops, and the array is not registered with the form. 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. This prop is no longer required and will be removed in the next major version. | |
rules | Object | The same validation rules API as for register, which includes: required, minLength, maxLength, validate. In case of validation error, the root property is appended to formState.errors?.fieldArray?.root 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 } | This object contains the defaultValue and key for your component. When a field entry includes disabled: true, the disabled attribute is automatically propagated to the registered input. |
append | (obj: object | object[], focusOptions) => void | Append input/inputs to the end of your fields and focus. The input value will be registered during this action. Important: append data is required and not partial. |
prepend | (obj: object | object[], focusOptions) => void | Prepend input/inputs to the start of your fields and focus. The input value will be registered during this action. Important: prepend data is required and not partial. |
insert | (index: number, value: object | object[], focusOptions) => void | Insert input/inputs at particular position and focus. Important: insert data is required and not partial. |
swap | (from: number, to: number) => void | Swap input/inputs position. |
move | (from: number, to: number) => void | Move input/inputs to another position. |
update | (index: number, obj: object) => void | Update input/inputs at a particular position, updated fields will get unmounted and remounted. If this is not desired behavior, please use setValue API instead. Important: update data is required and not partial. |
replace | (obj: object[]) => void | Replace the entire field array values. |
remove | (index?: number | number[]) => void | Remove input/inputs at particular position, or remove all when no index provided. |
Rules
-
useFieldArrayautomatically generates a unique identifier namedidwhich is used forkeyprop. For more information 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.
onClick={() => {append({ test: 'test' });remove(0);}}// ✅ Better solution: the remove action is happened after the second renderReact.useEffect(() => {remove(0);}, [remove])onClick={() => {append({ test: 'test' });}} -
Each
useFieldArrayis unique and has its own state update, which means you should not have multiple useFieldArray with the samename. -
Each input name needs to be unique, if you need to build checkbox or radio with the same name then use it with
useControllerorController. -
Does not support flat field array.
-
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. -
A
disabledproperty on a field entry is propagated to the registered input as thedisabledHTML attribute. This allows you to disable individual rows without manual prop threading.const { fields, append } = useFieldArray({ control, name: "test" })// append a disabled fieldappend({ value: "readonly", disabled: true })// the registered input will receive disabled automatically{fields.map((field, index) => (<input key={field.id} {...register(`test.${index}.value`)} />))} -
When you append, prepend, insert and update the field array, the obj can't be empty object
{}rather need to supply all your input's 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 detail.
-
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 structure or 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 <input {...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 to star and support it.