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: Do not support dynamic name. |
control | Object | control object provided by useForm . It's optional if you are using FormContext. | |
shouldUnregister | boolean | Whether Field Array will be unregistered after unmount. | |
keyName | string = id | Name of the attribute with autogenerated identifier to use as the | |
rules | Object | The same validation required, minLength, maxLength, validate
In case of validation error, the Important: This is only applicable to built-in validation only Resolvers are yet to supportuseFieldArray root level validation. |
Examples
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 FormContext) 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
Name | Type | Description |
---|---|---|
fields | object & { id: string } | This object contains the defaultValue and key for your component. |
append |
| 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 |
| Swap input/inputs position. |
move |
| Move input/inputs to another position. |
update |
| Update input/inputs at a particular position, updated fields will get unmount and remount. If this is not desired behavior, please use Important: update data is required and not partial. |
replace |
| Replace the entire field array values. |
remove |
| Remove input/inputs at particular position, or remove all when no index provided. |
Rules
useFieldArray
automatically generates a unique identifier namedid
which is used forkey
prop. For more information why this is required: https://reactjs.org/docs/lists-and-keys.html#keysThe
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 recommend to not stack actions one after another.
onClick={() => { append({ test: 'test' }); remove(0); }} // ✅ Better solution: the remove action is happened 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 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
useController
orcontroller
.Does not support flat field array.
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 register input
name
, you will have to cast them asconst
<input key={field.id} {...register(`test.${index}.test` as const)} />
we do not support circular reference. Refer to this this Github issue for more detail.
for nested field array, you will have to cast the field array by its name.
const { fields } = useFieldArray({ name: `test.${index}.keyValue` as 'test.0.keyValue' });
Examples
import React from "react"; 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 } = 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> ); }
Video
The following video explains the basic usage of useFieldArray
.
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 ( <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<FormValues>(); 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.