Skip to content

useFieldArray

Manage dynamic field arrays with optimized performance.

</> 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


NameTypeRequiredDescription
namestringName of the field array. Note: Dynamic names are not supported.
controlObjectcontrol object provided by useForm. It's optional if you are using FormProvider.
shouldUnregisterbooleanWhether the Field Array will be unregistered after unmounting.
disabledbooleanSince v7.79.0 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.
keyNamestring = "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.
rulesObjectSince 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?.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) => (
<input
key={field.id} // important to include key with field's id
{...register(`test.${index}.value`)}
/>
))}
);
}

Return


NameTypeDescription
fieldsobject & { id: string }This object contains the defaultValue and key for your component. Since v7.80.0 When a field entry includes disabled: true, the disabled attribute is automatically propagated to the registered input.
append(obj: object | object[], focusOptions) => voidAppend 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, focusIndex, focusName }.
prepend(obj: object | object[], focusOptions) => voidPrepend 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, focusIndex, focusName }.
insert(index: number, value: object | object[], focusOptions) => voidInsert 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, focusIndex, focusName }.
swap(from: number, to: number) => voidSwap the positions of inputs.
move(from: number, to: number) => voidMove an input or inputs to another position.
update(index: number, obj: object) => voidSince 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[]) => voidSince v7.15.0 Replace the entire field array's values.
remove(index?: number | number[]) => voidRemove an input or inputs at a particular position, or remove all if no index is provided.

Rules


  • useFieldArray automatically generates a unique identifier named id which is used for the key prop. For more information on why this is required: https://react.dev/learn/rendering-lists

    The field.id (and not index) 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 happens after the second render
    React.useEffect(() => {
    remove(0);
    }, [remove])
    onClick={() => {
    append({ test: 'test' });
    }}
  • Each useFieldArray is unique and has its own state update, which means you should not have multiple useFieldArray instances with the same name.

  • Each input name needs to be unique. If you need to build a checkbox or radio button with the same name, use it with useController or Controller.

  • Does not support flat field arrays.

  • shouldUnregister: true is not supported. Field array relies on inputs being mounted and unmounted to manage its internal state — enabling shouldUnregister causes newly added fields to be unregistered on re-render, so their values are lost. Avoid combining useFieldArray with shouldUnregister: true.

  • A disabled property on a field entry is propagated to the registered input as the disabled HTML attribute. This allows you to disable individual rows without manual prop threading.

    const { fields, append } = useFieldArray({ control, name: "test" })
    // append a disabled field
    append({ 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 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 as const:

    <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`)} />
<Controller
render={({ field }) => <input {...field} />}
name={`test.${index}.lastName`}
control={control}
/>
<button type="button" onClick={() => remove(index)}>
Delete
</button>
</li>
))}
</ul>
<button
type="button"
onClick={() => append({ firstName: "bill", lastName: "luo" })}
>
append
</button>
<input type="submit" />
</form>
)
}
import * as React from "react"
import { useForm, useFieldArray, useWatch, Control } from "react-hook-form"
type FormValues = {
cart: {
name: string
price: number
quantity: number
}[]
}
const Total = ({ control }: { control: Control<FormValues> }) => {
const formValues = useWatch({
name: "cart",
control,
})
const total = formValues.reduce(
(acc, current) => acc + (current.price || 0) * (current.quantity || 0),
0
)
return <p>Total Amount: {total}</p>
}
export default function App() {
const {
register,
control,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
defaultValues: {
cart: [{ name: "test", quantity: 1, price: 23 }],
},
mode: "onBlur",
})
const { fields, append, remove } = useFieldArray({
name: "cart",
control,
})
const onSubmit = (data: FormValues) => console.log(data)
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => {
return (
<div key={field.id}>
<section className={"section"} key={field.id}>
<input
placeholder="name"
{...register(`cart.${index}.name` as const, {
required: true,
})}
className={errors?.cart?.[index]?.name ? "error" : ""}
/>
<input
placeholder="quantity"
type="number"
{...register(`cart.${index}.quantity` as const, {
valueAsNumber: true,
required: true,
})}
className={errors?.cart?.[index]?.quantity ? "error" : ""}
/>
<input
placeholder="value"
type="number"
{...register(`cart.${index}.price` as const, {
valueAsNumber: true,
required: true,
})}
className={errors?.cart?.[index]?.price ? "error" : ""}
/>
<button type="button" onClick={() => remove(index)}>
DELETE
</button>
</section>
</div>
)
})}
<Total control={control} />
<button
type="button"
onClick={() =>
append({
name: "",
quantity: 0,
price: 0,
})
}
>
APPEND
</button>
<input type="submit" />
</form>
</div>
)
}
import * as React from "react"
import { useForm, useFieldArray, useWatch } from "react-hook-form"
export default function App() {
const { control, handleSubmit } = useForm()
const { fields, append, update } = useFieldArray({
control,
name: "array",
})
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
{fields.map((field, index) => (
<Edit
key={field.id}
control={control}
update={update}
index={index}
value={field}
/>
))}
<button
type="button"
onClick={() => {
append({ firstName: "" })
}}
>
append
</button>
<input type="submit" />
</form>
)
}
const Display = ({ control, index }) => {
const data = useWatch({
control,
name: `array.${index}`,
})
return <p>{data?.firstName}</p>
}
const Edit = ({ update, index, value, control }) => {
const { register, handleSubmit } = useForm({
defaultValues: value,
})
return (
<div>
<Display control={control} index={index} />
<input
placeholder="first name"
{...register(`firstName`, { required: true })}
/>
<button
type="button"
onClick={handleSubmit((data) => update(index, data))}
>
Submit
</button>
</div>
)
}
import React from "react"
import { useForm, useWatch, useFieldArray, Control } from "react-hook-form"
const ConditionField = ({ control, index, register }) => {
const output = useWatch({
name: "data",
control,
defaultValue: "yay! I am watching you :)",
})
return (
<>
{output[index]?.name === "bill" && (
<input {...register(`data[${index}].conditional`)} />
)}
<input
{...register(`data[${index}].easyConditional`)}
style={{ display: output[index]?.name === "bill" ? "block" : "none" }}
/>
</>
)
}
const UseFieldArrayUnregister = () => {
const { control, handleSubmit, register } = useForm({
defaultValues: {
data: [{ name: "test" }, { name: "test1" }, { name: "test2" }],
},
mode: "onSubmit",
shouldUnregister: false,
})
const { fields } = useFieldArray({
control,
name: "data",
})
const onSubmit = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((data, index) => (
<>
<input {...register(`data[${index}].name`)} />
<ConditionField control={control} register={register} index={index} />
</>
))}
<input type="submit" />
</form>
)
}
import React from 'react';
import { useForm, useFieldArray } from 'react-hook-form';
const App = () => {
const { register, control } = useForm({
defaultValues: {
test: [{ value: '1' }, { value: '2' }],
},
});
const { fields, prepend, append } = useFieldArray({
name: 'test',
control,
});
return (
<form>
{fields.map((field, i) => (
<input key={field.id} {...register(`test.${i}.value` as const)} />
))}
<button
type="button"
onClick={() => prepend({ value: '' }, { focusIndex: 1 })}
>
prepend
</button>
<button
type="button"
onClick={() => append({ value: '' }, { focusName: 'test.0.value' })}
>
append
</button>
</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 (
<Controller
control={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 starring and supporting it.

Edit