React Hook Form is a popular library that helps us deal with forms and keep their code consistent across the whole application. In this article, we look into how to allow the user to shape the form to some extent and create data structures that are recursive. In the end, we get the following form:
If you want to learn the basics of React Hook Form instead, check out Building forms with React Hook Form and TypeScript
Dealing with arrays of inputs
Let’s start with defining an interface that describes the form values.
FriendsFormValues.tsx
1 2 3 4 5 6 |
interface FriendsFormValues { name: string; friends: { name: string }[]; } export default FriendsFormValues; |
Also, let’s create the basics of our form that uses the <FormProvider /> component. Thanks to doing that, we will be able to access the context of the form in the components we create later.
FriendsForm.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import React from 'react'; import { FormProvider } from 'react-hook-form'; import useFriendsForm from './useFriendsForm'; import FriendsFormField from './FriendsFormField/FriendsFormField'; import styles from './FriendsForm.module.scss'; const FriendsForm = () => { const { handleSubmit, methods } = useFriendsForm(); return ( <FormProvider {...methods}> <form onSubmit={handleSubmit} className={styles.form}> <FriendsFormField /> <button type="submit">Submit</button> </form> </FormProvider> ); }; export default FriendsForm; |
We use SCSS modules to style all of the componentes in this article.
We initiate the form with simple values. Then, if the user clicks on the submit button, we log the values to the console.
useFriendsForm.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import FriendsFormValues from './FriendsFormValues'; import { useForm } from 'react-hook-form'; function useFriendsForm() { const methods = useForm<FriendsFormValues>({ defaultValues: { name: '', friends: [], }, }); const handleSubmit = (values: FriendsFormValues) => { console.log(values); }; return { methods, handleSubmit: methods.handleSubmit(handleSubmit), }; } export default useFriendsForm; |
Using the useFieldArray hook
In our form, we have the friends array. In our case, we want users to be able to add, modify, and remove elements from this array. Fortunately, React Hook Form has a hook designed just for this purpose.
1 2 3 4 5 6 |
const { control, register } = useFormContext<FriendsFormValues>(); const { fields, append, remove } = useFieldArray<FriendsFormValues>({ control, name: 'friends', }); |
Above, we use the control object that contains methods for registering components into React Hook Form. We also provide the name of the field we want to handle.
React Hook Form currently does not support using useFieldArray to handle arrays of primitive values such as strings or numbers. Because of that, our friends property needs to be an array of objects.
The useFieldArray hook returns various useful objects and functions that allow us to interact with the array. In this application, we use the following:
- fields – an array of objects containing the default value of a particular element and an autogenerated id to use as a key,
- append – a function we use to add another element to our array,
- remove – we can use this function to remove a particular element.
We can create a custom hook that uses all of the above functionalities.
useFriendsFormField.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import { useFieldArray, useFormContext } from 'react-hook-form'; import FriendsFormValues from '../FriendsFormValues'; function useFriendsFormField() { const { control, register } = useFormContext<FriendsFormValues>(); const { fields, append, remove } = useFieldArray<FriendsFormValues>({ control, name: 'friends', }); const addNewFriend = () => { append({ name: '', }); }; const removeFriend = (friendIndex: number) => () => { remove(friendIndex); }; return { fields, register, addNewFriend, removeFriend, }; } export default useFriendsFormField; |
Now, we can create a component that takes advantage of all of the above functions.
FriendsFormField.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import React from 'react'; import styles from './FriendsFormField.module.scss'; import useFriendsFormField from './useFriendsFormField'; const FriendsFormField = () => { const { fields, register, addNewFriend, removeFriend } = useFriendsFormField(); return ( <div className={styles.wrapper}> <div className={styles.labelContainer}> <input {...register('name')} placeholder="Name" /> <button type="button" onClick={addNewFriend} className={styles.addPropertyButton} > + Add friend </button> </div> {fields.map((field, index) => ( <div key={field.id} className={styles.propertyContainer}> <button type="button" onClick={removeFriend(index)} className={styles.removePropertyButton} > - </button> <input {...register(`friends.${index}.name`)} placeholder="Name" /> </div> ))} </div> ); }; export default FriendsFormField; |
Above, the most crucial part is the following where we use the autogenerated field.id and the register function:
1 2 3 4 5 6 7 8 9 10 11 12 |
{fields.map((field, index) => ( <div key={field.id} className={styles.propertyContainer}> <button type="button" onClick={removeFriend(index)} className={styles.removePropertyButton} > - </button> <input {...register(`friends.${index}.name`)} placeholder="Name" /> </div> ))} |
Please notice that we use register(`friends.${index}.name`)} instead of register(`friends[${index}].name`). This started to be the required approach since React Hook Form v7.
After doing all of the above, we end up with the following form:
Working with recursive data structures
So far, we’ve been working with a simple array of objects. Let’s make it a bit more interesting by making our data structure recursive.
If you want to know more about recursion, check out Using recursion to traverse data structures. Execution context and the call stack
FriendsFormValues.tsx
1 2 3 4 5 6 |
interface FriendsFormValues { name: string; friends: FriendsFormValues[]; } export default FriendsFormValues; |
Let’s modify our FriendsFormField component to accept a prefix so that we can keep track of how deep we are in our form. Some of its possible values are:
- an empty string,
- friends[0].,
- friends[1].friends[0].,
- friends[2].friends[1].friends[0].,
- …
FriendsFormField.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import React, { FunctionComponent } from 'react'; import useFriendsFormField from './useFriendsFormField'; interface Props { prefix?: string; } const FriendsFormField: FunctionComponent<Props> = ({ prefix = '' }) => { const { fields, register, addNewFriend, removeFriend, nameInputPath } = useFriendsFormField(prefix); // ... }; export default FriendsFormField; |
Unfortunately, React Hook Form currently does not handle circular references in the data well with TypeScript as stated in the official documentation. Because of that, we declare our FriendsFormValues again without the circular references.
It is possible that React Hook Form 8 will improve the above situation.
useFriendsFormField.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import { useFieldArray, useFormContext } from 'react-hook-form'; interface FriendsFormValues { name: string; friends: { name: string }[]; } function useFriendsFormField(prefix: string) { const { control, register } = useFormContext<FriendsFormValues>(); const nameInputPath = `${prefix}name` as 'name'; const friendsArrayInputPath = `${prefix}friends` as 'friends'; const { fields, append, remove } = useFieldArray({ control, name: friendsArrayInputPath, }); const addNewFriend = () => { append({ name: '', }); }; const removeFriend = (friendIndex: number) => () => { remove(friendIndex); }; return { fields, register, addNewFriend, removeFriend, nameInputPath, }; } export default useFriendsFormField; |
Above, we combine the current prefix with “name” and “friends”. For example, if our prefix is friends[0].friends[1]., we get friends[0].friends[1].name and friends[0].friends[1].friends.
We now have everything we need to define the FriendsFormField component.
FriendsFormField.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
import React, { FunctionComponent } from 'react'; import styles from './FriendsFormField.module.scss'; import useFriendsFormField from './useFriendsFormField'; interface Props { prefix?: string; } const FriendsFormField: FunctionComponent<Props> = ({ prefix = '' }) => { const { fields, register, addNewFriend, removeFriend, nameInputPath } = useFriendsFormField(prefix); return ( <div className={styles.wrapper}> <div className={styles.labelContainer}> <input {...register(nameInputPath)} placeholder="Name" /> <button type="button" onClick={addNewFriend} className={styles.addPropertyButton} > + Add friend </button> </div> {fields.map((field, index) => ( <div key={field.id} className={styles.propertyContainer}> <button type="button" onClick={removeFriend(index)} className={styles.removePropertyButton} > - </button> <FriendsFormField prefix={`${prefix}friends.${index}.`} /> </div> ))} </div> ); }; export default FriendsFormField; |
The crucial thing is that FriendsFormField renders FriendsFormField recursively. To generate a new prefix, we combine the current prefix with friends.${index}.:
1 2 3 4 5 6 7 8 9 10 11 12 |
{fields.map((field, index) => ( <div key={field.id} className={styles.propertyContainer}> <button type="button" onClick={removeFriend(index)} className={styles.removePropertyButton} > - </button> <FriendsFormField prefix={`${prefix}friends.${index}.`} /> </div> ))} |
Thanks to the above approach, we end up with the following form:
Summary
In this article, we’ve managed to create dynamic forms that deal with recursive data structures using React Hook Form and TypeScript. To do that, we had to learn about the useFieldArray hook.
While React Hook Form was created just three years ago, it quickly became popular and recently caught up with Formik, its main competitor.
If you want to know how to build the above form with Formik, check out Dynamic and recursive forms with Formik and TypeScript
Also, React Hook Form seems to be more actively maintained, which can be a critical factor when choosing a suitable library for a project.
All of the above make the React Hook Form a fitting solution for form in our new React projects, even when dealing with recursive data structures.
Very basic example, I am looking for an example that adds “a set of columns per row” e.g. add friend (first-name, last-name, age)
Feel free to add more properties to the FriendsFormValues interface.
Great one! I have been struggling with putting something similar together
Do you have CSS styles used in this module available somewhere?