Working with arrays in forms (dynamically adding fields) using the react-hook-form library

Hello friends!

In this article I want to show you how to develop a form with dynamic addition of fields on React.js using the react-hook-form library and validation of fields using the yup library using a specific example.

Forms are an integral part of web development, and efficiently handling user input is a key aspect of creating interactive applications. The React Hook Form library provides developers with a powerful toolkit to simplify working with forms in React applications.

Project preparation and setup:

Let's deploy a new React.js app with Typescript using npm:

open terminal and run the following command (make sure you have npm installed):

npx create-react-app react_form --template typescript

Then we will install the necessary libraries into our application:

Open the terminal in the app and install the React Hook Form library using the command:

npm install react-hook-form

The next thing we need to do is install the YUP library to validate our form using the command:

npm i yup

The YUP library is a powerful form validation tool. In this article, we will cover it in basic terms.

We will also need to install a special wrapper (adapter) for YUP and React Hook Form to work together using the command:

npm install @hookform/resolvers/yup

This completes our preparation and we can start coding.

To work with dynamic fields in React Hook Form there is a useFieldArray hook, this is one of the key hooks of the library, allowing you to manage dynamic form fields.

To use useFieldArray, you need a form controller, which you get from the useForm hook. You must pass this controller and the name of the field array to useFieldArray using the following parameters:

Example of using useFieldArray:

const { control, handleSubmit } = useForm();

const { fields, append, remove, move, update } = useFieldArray({

  control,

  name: 'questions' // имя массива полей в форме

});

More details about each method:

  • fields is an array of objects containing the field values ​​and helper methods for working with this array.

  • append – method for adding a new element to the fields array. Takes an object with the field values ​​as an argument.

  • remove – method for removing an element from the array of fields by index.

  • move – method for moving an element in the fields array. Takes two arguments: the current index of the element and the index where the element should be moved.

  • update – method for updating the value of a field in the fields array. Takes two arguments: the element index and the new value.

Let's look at how the form works using the example of a questionnaire with nested fields and dynamic answer options:

  1. Let's create a common form component:


import React from 'react';
import {Resolver, useFieldArray, useForm} from 'react-hook-form';
import {yupResolver} from '@hookform/resolvers/yup';
import * as yup from 'yup';
import s from './styles.module.scss';
import {Answers} from "../ansvers/ansvers";
import {Questions} from "../questions/questions";

interface Question {
  questionText: string;
  options: Option[];
}
interface Option {
    optionText: string;
}
export interface FormData {
  questions: Question[];
}
//  Создаем минимальную схему валидации с помощью YUP
const schema = yup.object().shape({
    questions: yup.array().of(
        yup.object().shape({
            questionText: yup.string().required('Введите текст вопроса'),
            options: yup.array().of(
                yup.object().shape({
                    optionText: yup.string().required('Введите текст варианта ответа')
                })
            )
        })
    )
});
export const UseFieldArray = () => {

    // Получаем необходимые методы из хука UseForm
    const { control, handleSubmit } = useForm<FormData>({
        resolver: yupResolver(schema) as Resolver<FormData>
    });

    // Получаем необходимые методы из хука UseFieldArray
    const { fields, append, remove, move  } = useFieldArray<FormData, 'questions'>({
        control,
        name: 'questions'
    });

    // Функция для сабмита формы
    const onSubmit = (data: FormData) => {
        console.log(data);
    };

    // Функция для перемещения варианта ответа вверх в массиве
    const moveUp = (index: number) => {
        if (index > 0) {
            move(index, index - 1)
        }
    };

    // Функция для перемещения варианта ответа вниз в массиве
    const moveDown = (index: number) => {
        if (index < fields.length - 1) {
            move(index, index + 1)
        }
    };

    // Функция для удаления вопроса
    const removeQuestion = (index: number) => {
        remove(index)
    }

    // Функция для добавления нового вопроса
    const addQuestion = () => {
        append({questionText: '', options: []})
    }

    return (
        <form className={s.wrapper_form} onSubmit={handleSubmit(onSubmit)}>
            {fields.map((fieldT, index) => (
                <div className={s.content_form} key={fieldT.id}>
                    <div className={s.question_container}>
                        <label htmlFor={`questions[${index}].questionText`}>Вопрос {index + 1}</label>
                        <div className={s.question}>
                            <Questions questionFieldIndex={index} control={control}/>
                            <button className={s.button} type="button" onClick={() => moveUp(index)}>Вверх</button>
                            <button className={s.button} type="button" onClick={() => moveDown(index)}>Вниз</button>
                            <button className={s.button} type="button" onClick={() => removeQuestion(index)}>Удалить
                                вопрос
                            </button>
                        </div>
                    </div>
                    <Answers
                        control={control}
                        parentFieldIndex={index}
                    />
                    </div>
            ))}
            <div className={s.button_container}>
                <button className={s.button} type="button"
                        onClick={addQuestion}>Добавить вопрос
                </button>
                <button className={s.button} type="submit">Отправить</button>
            </div>
        </form>
);
}

Let's add a little beauty to our form so that it doesn't hurt the eye so much:


.wrapper_form {
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.1);
  width: 700px;
  margin: 0 auto;
  gap: 10px;
  padding: 10px;
}
.content_form {
  margin: 15px;
  border-radius: 5px;
  padding: 5px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.1);
}
.question_container {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.question {
  display: flex;
  width: 100%;
  justify-content: space-between;
  gap: 10px;
}

.input {
  width: 60%;
  border-radius: 5px;
}
.answers {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin: 10px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.1);
}
.answer {
  display: flex;
  justify-content: space-between;
  margin: 10px;
}

.answer_input {
  width: 80%;
  border-radius: 5px;
}
.button_container {
  display: flex;
  justify-content: space-between;
  margin: 5px;
  gap: 10px;
}

.button {
  background-color: #05A552;
  padding: 10px;
  border: none;
  border-radius: 5px;
  color: white;
}
  1. Let's move the input with the question into a separate component for greater clarity:

import React from 'react';
import {Control, Controller} from 'react-hook-form';
import s from './styles.module.scss';
import {FormData} from "../useFieldArray/useFieldArray";

interface IProps {
    control: Control<FormData>;
    questionFieldIndex: number;
}
export const 
= ({
                              control,
                              questionFieldIndex
                          }: IProps) => {

    return ( <Controller
                control={control}
            // Обратите внимание для корректной работы нам необходим индекс текущего элемента
                name={`questions.${questionFieldIndex}.questionText`}
                render={({field}) => (
                <input className={s.input} {...field} />
           )}
          />
    )
}

here we have transferred the form control to another component via the controller, so we can divide large forms into smaller ones, which will improve the readability of the code and make it possible to reuse this component in other places

styles for the Questions component:

.wrapper_form {
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.1);
  width: 700px;
  margin: 0 auto;
  gap: 10px;
  padding: 10px;
}
.content_form {
  margin: 15px;
  border-radius: 5px;
  padding: 5px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.1);
}
.question_container {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.question {
  display: flex;
  width: 100%;
  justify-content: space-between;
  gap: 10px;
}

.input {
  width: 60%;
  border-radius: 5px;
}
.answers {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin: 10px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.1);
}
.answer {
  display: flex;
  justify-content: space-between;
  margin: 10px;
}

.answer_input {
  width: 80%;
  border-radius: 5px;
}
.button_container {
  display: flex;
  justify-content: space-between;
  margin: 5px;
  gap: 10px;
}

.button {
  background-color: #05A552;
  padding: 10px;
  border: none;
  border-radius: 5px;
  color: white;
}
  1. We will also move the array of answer options into a separate component and also set up control for the array of answers using useFieldArray:

import React from 'react';
import {Control, Controller, useFieldArray} from 'react-hook-form';
import s from './styles.module.scss';
import {FormData} from '../useFieldArray/useFieldArray'

interface IProps {
    control: Control<FormData>;
    parentFieldIndex: number;
}
export const Answers = ({
                              control,
                              parentFieldIndex
                          }: IProps) => {
    const { fields, append, remove } = useFieldArray({
        control,
        name: `questions.${parentFieldIndex}.options`
    });

    const addAnsver = () => {
        append({
            optionText: ""
        })
    }
    const remoutAnsver = (index: number) => {
        remove(index)
    }

    return (
          <div>
            {fields.map((field, index) => (
                    <div className={s.answers} key={field.id}>
                        <label htmlFor={`questions[${index}].options[${index}]`}>Вариант
                            ответа {index + 1}</label>
                        <div className={s.answer}>
                            <Controller
                                control={control}
                                name={`questions.${parentFieldIndex}.options.${index}.optionText`}
                                render={({field}) => (
                                    <input className={s.input} {...field} />
                                )}
                            />
                            <button className={s.button} type="button"
                                    onClick={() => remoutAnsver(index)}>Удалить
                            </button>
                        </div>
                    </div>
                         )
            )}
            <div className={s.button_container}>
                <button className={s.button} type="button" onClick={addAnsver}>
                    Добавить вариант ответа
                </button>
            </div>
            </div>
    );
}

styles for the Answers component:

.input {
  width: 60%;
  border-radius: 5px;
}
.answers {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin: 10px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.1);
}
.answer {
  display: flex;
  justify-content: space-between;
  margin: 10px;
}

.answer_input {
  width: 80%;
  border-radius: 5px;
}
.button_container {
  display: flex;
  justify-content: space-between;
  margin: 5px;
  gap: 10px;
}

.button {
  background-color: #05A552;
  padding: 10px;
  border: none;
  border-radius: 5px;
  color: white;
}

The first thing you need to do is extract the necessary methods from the useForm and useFieldArray hooks, such as handleSubmit, control, fields, append and remove.

When we click the “Add Question” button, we use the append method from the useFieldArray hook to add a new item to the end of our array. This takes the new item and adds it to the end of the array.

When we press the Up or Down button, we use the move method to move elements within the array. It takes two parameters: from – the index of the element to be moved, and to – the index of the position to which the element should be moved.

If we click the “Delete Question” button, then using the remove method we remove an element from the array at the specified index. The remove method takes one parameter – index, which indicates the index of the element to be removed.

If we need to insert an element at a specific location, the insert method will help us. It allows us to insert a new item at a specified index position in the array. All elements located after the specified position will be shifted to the right.

Working with the response array using useFieldArray is exactly the same as with the main array, with only 1 addition we need to know the index of our object in the main array to work correctly with the subarray

After calling any of these methods, React automatically updates the component to reflect the movement of the element in the UI.

Conclusion:

In this article, we looked at how to use the react-hook-form library and the useFieldArray hook to work with forms that allow you to dynamically add fields. We created a sample questionnaire with nested question fields and answer options, and showed how to add, remove, and move elements in an array. React Hook Form provides convenient tools for working with forms in React applications, allowing you to manage dynamic fields and handle user input with a simple and efficient API. Using the useFieldArray hook simplifies managing arrays of form fields, making the process of adding, removing, and moving fields easier and more intuitive.

I hope this article helped you better understand how to use react-hook-form and the useFieldArray hook to work with arrays in forms in React applications.

Link to github

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *