Skip to content

useFieldArray

React hooks for Field Array

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

NameTypeRequiredDescription
namestring

Name of the field array. Note: Do not support dynamic name.

controlObjectcontrol object provided by useForm. It's optional if you are using FormContext.
shouldUnregisterboolean

Whether Field Array will be unregistered after unmount.

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.

rulesObject

The same validation rules API as for register, which includes:

required, minLength, maxLength, validate

useFieldArray({
  rules: { minLength: 4 }
})

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 only.

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

NameTypeDescription
fieldsobject & { id: string }This object contains the defaultValue and key for your component.
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) => voidSwap input/inputs position.
move(from: number, to: number) => voidMove input/inputs to another position.
update(index: number, obj: object) => void

Update input/inputs at a particular position, updated fields will get unmount and remount. If this is not desired behavior, please use setValue API instead.

Important: update data is required and not partial.

replace(obj: object[]) => voidReplace the entire field array values.
remove(index?: number | number[]) => voidRemove input/inputs at particular position, or remove all when no index provided.

Rules

  • useFieldArray automatically generates a unique identifier named id which is used for key prop. For more information why this is required: https://reactjs.org/docs/lists-and-keys.html#keys

    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 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 same name.

  • Each input name needs to be unique, if you need to build checkbox or radio with the same name then use it with useController or controller.

  • 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 as const

    <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, 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';

type FormValues = {
  data: { name: string }[];
};

const ConditionField = ({
  control,
  index,
  register,
}: {
  control: Control<FormValues>;
  index: number;
}) => {
  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: React.FC = () => {
  const { control, handleSubmit, register } = useForm<FormValues>({
    defaultValues: {
      data: [{ name: 'test' }, { name: 'test1' }, { name: 'test2' }],
    },
    mode: 'onSubmit',
    shouldUnregister: false,
  });
  const { fields } = useFieldArray({
    control,
    name: 'data',
  });
  const onSubmit = (data: FormValues) => 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<{
    test: { value: string }[];
  }>({
    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

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.

Edit