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