Skip to content

useFieldArray

React hooks for Field Array

useFieldArray:
({ control?: Control, name: string, keyName?: string = 'id' }) => object

Custom hook for working with uncontrolled Field Arrays (dynamic inputs). The motivation is to provide better user experience and form performance. You can watch this short video to compare controlled vs uncontrolled Field Array.

Props

NameTypeRequiredDescription
namestring

Name of the field. Important: make sure name is in object shape: name=test.0.name as we don't support flat arrays.

controlObjectcontrol object provided by useForm. It's optional if you are using FormContext.
keyNamestring = 'id'field array key value, default to "id", you can change the key name.
shouldUnregisterboolean = false

Field Array will be unregistered after unmount.

function Test() {
  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
    // keyName: "id", default to "id", you can change the key name
  });

  return (
    {fields.map((field, index) => (
      <input
        key={field.id} // important to include key with field's id
        {...register(`test.${index}.value`)} 
      />
    ))}
  );
}

Rules

  • 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) => (
      <div key={field.id}>
        <input ... />
      </div>
    ))}
    // ✅ correct:
    {fields.map((field, index) => <input key={field.id} ... />)}
    // ❌ incorrect:
    {fields.map((field, index) => <input key={index} ... />)}
    

  • You can not call actions one after another. Actions need to be triggered per render.

    // ❌ The following is not correct
    handleChange={() => {
      if (fields.length === 2) {
        remove(0);
      }
      append({ test: 'test' });
    }}
    
    // ✅ The following is correct and second action is triggered after next render
    handleChange={() => {
      append({ test: 'test' });
    }}
    
    React.useEffect(() => {
      if (fields.length === 2) {
        remove(0);
      }
    }, [fields])
                
  • 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' });

Return

NameTypeDescription
fieldsobject & { id: string }This object contains the defaultValue and key for all your inputs. It's important to assign defaultValue to the inputs.

Important: Because each input can be uncontrolled, id is required with mapped components to help React to identify which items have changed, are added, or are removed.

{fields.map((data, index) =>
  <input
    key={data.id}
    name={`data.${index}.value`}
  />;
);}
append(obj: object | object[], { shouldFocus?: boolean; focusIndex?: number; focusName?: string; }) => void

Append input/inputs to the end of your fields and focus. The input value will be registered during this action.

prepend(obj: object | object[], { shouldFocus?: boolean; focusIndex?: number; focusName?: string; }) => void

Prepend input/inputs to the start of your fields and focus. The input value will be registered during this action.

insert(index: number, value: object | object[], { shouldFocus?: boolean; focusIndex?: number; focusName?: string; }) => void

Insert input/inputs at particular position and focus.

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) => voidUpdate input/inputs at particular position.
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.
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>
  );
}
import React from 'react';
import { useForm, useWatch, useFieldArray, Control } from 'react-hook-form';

const ConditionField = ({
  control,
  index,
  register,
}: {
  control: Control;
  index: number;
}) => {
  const output = useWatch<any>({
    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 = () => {
  type FormValues = {
    data: { name: string }[];
  };

  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>
  );
};

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 * as React from "react";
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 { handleSubmit, control, register } = useForm();
  const { fields, append, prepend } = useFieldArray({
    control,
    name: "test"
  });
  const onSubmit = (data) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {fields.map((field, index) => (
        <section key={field.id}>
          <input
            type="checkbox"
            value="on"
            {...register(`test.${index}.checkbox`)}
          />
          <ConditionalInput {...{ control, index, field }} />
        </section>
      ))}

      <button
        type="button"
        onClick={() => append({ firstName: "append value" }) }
      >
        append
      </button>
      <input type="submit" />
    </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. You can achieve this by merge with useWatch or watch's result.

import * as React from "react";
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)} />;
      })}

      <button
        type="button"
        onClick={() =>
          append({
            name: "bill"
          })
        }
      >
        Append
      </button>
    </form>
  );
}

Conditional inputs

useFieldArray is uncontrolled by default, which means defaultValue is rendered during input's mounting. This is different from the controlled form, which the local state is keeping updated during user interaction's onChange. When inputs get unmounted and remounted, you want to read what's in the current form values from getValue function. For individual input, you can safely use useWatch hook to retain input value as well.

import React from "react";
import { useForm, useFieldArray, Controller, useWatch } from "react-hook-form";

const Input = ({ name, control, register, index }) => {
  const value = useWatch({
    control,
    name
  });
  return <input {...register(`test.${index}.age`)} defaultValue={value} />;
};

function App() {
  const [show, setShow] = React.useState(true);
  const { register, control, getValues, handleSubmit } = useForm({
    defaultValues: {
      test: [{ firstName: "Bill", lastName: "Luo", age: "2" }]
    }
  });
  const { fields, remove } = useFieldArray({
    control,
    name: "test"
  });
  const onSubmit = (data) => console.log("data", data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {show && (
        <ul>
          {fields.map((item, index) => {
            return (
              <li key={item.id}>
                <input {...register(`test.${index}.firstName`)} />
                <Input
                  register={register}
                  control={control}
                  index={index}
                  name={`test.${index}.age`}
                />
              </li>
            );
          })}
        </ul>
      )}
      <section>
        <button
          type="button"
          onClick={() => {
            setShow(!show);
          }}
        >
          Hide
        </button>
      </section>

      <input type="submit" />
    </form>
  );
}
Edit