Dynamic Form with react-hook-form useFieldArray
Published: 2023-01-08 04:19:53

[✨ **Watch on YouTube**](https://youtu.be/QYVlkk6WMmc)  |  [🐙 GitHub](https://github.com/RodionChachura/react-toolkit)  |  [🎮 Demo](https://reactkit.radzion.com/dynamic-form)

Let's make a dynamic form with react-hook-form. Here we have a job application form where you can add a list of experiences.

![](form.png)

We use `yup` to define the schema for our form that matches the `JobApplicationForm` type. To validate the experience list, we pass the yup object to the `"array().of()"` sequence. In the `useJobApplicationForm`, we defined default values for the form to show one experience sub-form to the user.

```tsx
import { useForm } from "react-hook-form"
import * as yup from "yup"
import { yupResolver } from "@hookform/resolvers/yup"

export const bioMaxLength = 300
export const responsibilityMaxLength = 300

interface JobExperienceShape {
  position: string
  responsibility: string
}

export interface JobApplicationFormShape {
  name: string
  bio: string
  experience: JobExperienceShape[]
}

const schema: yup.SchemaOf<JobApplicationFormShape> = yup.object({
  name: yup.string().max(100).required(),
  bio: yup.string().max(bioMaxLength).required(),
  experience: yup
    .array()
    .of(
      yup.object({
        position: yup.string().min(4).required(),
        responsibility: yup
          .string()
          .min(10)
          .max(responsibilityMaxLength)
          .required(),
      })
    )
    .required(),
})

export const emptyExperience: JobExperienceShape = {
  position: "",
  responsibility: "",
}

export const useJobApplicationForm = () => {
  return useForm<JobApplicationFormShape>({
    mode: "onSubmit",
    resolver: yupResolver(schema),
    defaultValues: {
      experience: [emptyExperience],
    },
  })
}
```

Here we set up our form component with fields for general info and a section to list the experience. To work with the list of sub-forms, we leverage the `useFieldArray` hook. We pass the control function and the name of the field array and receive functions for managing a dynamic form.

We display sub-form by iterating over the fields array and displaying experience number with a remove button on the left side and content on the right. To remove an experience, we call the remove function, and to handle inputs, we pass to the register function a template string that includes the name of the sub-form, index, and field name. The same goes for the error message.

```tsx
export const ExperienceSection = ({
  form: {
    control,
    register,
    formState: { errors },
  },
}: Props) => {
  const { fields, append, remove } = useFieldArray({
    control,
    name: "experience",
  })

  return (
    <FormSection name="Experience">
      {fields.map((field, index) => (
        <VStack key={index} fullWidth gap={16}>
          <HStack fullWidth gap={24}>
            <VStack gap={8}>
              <ExperienceNumber size={manageElementSizeInPx}>
                <Text>{index + 1}</Text>
              </ExperienceNumber>
              <IconButton
                onClick={() => remove(index)}
                kind="alert"
                as="div"
                size="l"
                icon={<TrashIcon />}
              />
            </VStack>
            <VStack fullWidth gap={16}>
              <TextInput
                label="Position"
                {...register(`experience.${index}.position`)}
                error={errors.experience?.[index]?.position?.message}
                placeholder="Senior Front End Engineer"
              />
              <TextArea
                label="Responsibility"
                {...register(`experience.${index}.responsibility`)}
                error={errors.experience?.[index]?.responsibility?.message}
                rows={3}
                placeholder="I was responsible for ..."
                maxLength={responsibilityMaxLength}
              />
            </VStack>
          </HStack>
          <Line />
        </VStack>
      ))}
      <VStack alignItems="start">
        <OutlinedButton
          type="button"
          isRounded
          onClick={() => append(emptyExperience)}
        >
          Add experience
        </OutlinedButton>
      </VStack>
    </FormSection>
  )
}
```

Since we have the `onSubmit` mode for validating the form, we don't see the errors while filling up the form, but they show up when we try to submit the form.