Dynamic Form with react-hook-form useFieldArray

October 31, 2022

3 min read

Dynamic Form with react-hook-form useFieldArray
Watch on YouTube

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

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.

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.

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.