7

Creating recursive and dynamic forms with React Hook Form and TypeScript

 1 year ago
source link: https://wanago.io/2022/05/16/recursive-dynamic-forms-react-hook-form-typescript/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Creating recursive and dynamic forms with React Hook Form and TypeScript

JavaScript React

May 16, 2022

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:

form_friends.gif

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

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

{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:

form_simple_2.gif

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

{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:

form_friends.gif

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

Screenshot-from-2022-05-15-19-05-13.png

Also, React Hook Form seems to be more actively maintained, which can be a critical factor when choosing a suitable library for a project.

Screenshot-from-2022-05-15-19-05-21.png

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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK