DEV Community

Cover image for Dynamic forms with React Hook Form. πŸ“
Franklin Martinez
Franklin Martinez

Posted on

Dynamic forms with React Hook Form. πŸ“

This time, we will again create dynamic forms but now with the help of the react-hook-form library.

Note πŸ“£: You need to have knowledge in Typescript to follow this tutorial, as well as React JS.

You might be interested in this article, where we also do the same as in this post, but using the Formik library. πŸ˜‰

Β 

Table of contents.

πŸ“Œ Technologies to be used.

πŸ“Œ Creating the project.

πŸ“Œ First steps.

πŸ“Œ Creating the form object.

πŸ“Œ Creating the typing for the inputs.

πŸ“Œ Now we create the form object with the help of the typing.

πŸ“Œ Creating the validation schema for our form.

πŸ“Œ Function to generate the inputs.

πŸ“Œ Creating the form component.

πŸ“Œ Creating the components of each input.

πŸ“Œ Using our Form component.
πŸ“Œ Conclusion.

πŸ“Œ Demo.

πŸ“Œ Source Code.

Β 

πŸ’Š Technologies to be used.

  • React JS 18.2.0
  • TypeScript 4.9.3
  • React Hook Form 7.43.0
  • Vite JS 4.1.0
  • Tailwind CSS 3.2.4 (neither the installation nor the configuration process is displayed).

πŸ’Š Creating the project.

We will name the project: dynamic-forms-rhf (optional, you can name it whatever you like).

npm create vite@latest
Enter fullscreen mode Exit fullscreen mode

We create the project with Vite JS and select React with TypeScript.

Then we run the following command to navigate to the directory just created.

cd dynamic-forms-rhf
Enter fullscreen mode Exit fullscreen mode

Then we install the dependencies.

npm install
Enter fullscreen mode Exit fullscreen mode

Then we open the project in a code editor (in my case VS code).

code .
Enter fullscreen mode Exit fullscreen mode

πŸ’Š First steps.

Inside the src/App.tsx file we delete everything and create a component that displays a hello world.

const App = () => {
    return (
        <div>Hello world</div>
    )
}
export default App
Enter fullscreen mode Exit fullscreen mode

🚨 Note: Each time we create a new folder, we will also create an index.ts file to group and export all the functions and components of other files that are inside the same folder, so that these functions can be imported through a single reference, this is known as barrel file.

Let's create a layout, create a folder src/components and inside create a file Layout.tsx.

export const Layout = ({ children }: { children: JSX.Element | JSX.Element[] }) => {
    return (
        <>
            <h1 className='text-center my-10 text-5xl'>
                <span>Dynamic Form</span>
                <span className='font-bold bg-clip-text text-transparent  text-[#EC5990]'>
                    {' - '}
                    React Hook Form
                </span>
            </h1>

            <main className='grid sm:grid-cols-2 grid-cols-1 sm:mb-0 mb-10 gap-10 place-items-start justify-items-center px-5'>
                {children}
            </main>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

Now, inside the src/App.tsx file, we add the layout.

import { Layout } from './components'

const App = () => {

    return (
        <Layout>
            <span>Form</span>
        </Layout>
    )
}
export default App
Enter fullscreen mode Exit fullscreen mode

Then we are going to install the necessary packages.

  • react-hook-form, to handle the forms in an easier way.
  • yup, to handle form validations.
  • @hookform/resolvers, to integrate yup with react-hook-form.
npm install -E react-hook-form @hookform/resolvers yup
Enter fullscreen mode Exit fullscreen mode

Previously I had already done this same exercise of dynamic forms but using the Formik library, and the truth is very similar to what we are going to do, the only thing to change are the components such as the form and inputs.

πŸ’Š Creating the form object.

πŸ’Š Creating the typing for the inputs.

First, let's create the typing. We create a new folder src/types and create the index.ts file.

Now first we create the interface for the inputs, which can have even more properties, but these are enough to make this example.

The highlights are the last three properties of the InputProps interface:

  • typeValue: necessary since we need to tell Yup what type of value the input accepts.
  • validations: validations that will be set to Yup based on the input; I only put basic validations, although you can integrate more if you look in the Yup documentation.
    • The validation that may be more complicated for you may be oneOf, if you have not used Yup. This validation needs a reference or the name of another input to validate if both inputs contain the same content. An example of where to use this validation is in an input where you create a password and another one where you have to repeat password and both values have to match.
  • options: this property is necessary only if the input is a select or a group of radio type inputs.
export interface InputProps {
    type: 'text' | 'radio' | 'email' | 'password' | 'select' | 'checkbox'
    name: string
    value: string | number | boolean
    placeholder?: string
    label?: string

    typeValue?:  'boolean' | 'number'
    validations?: Validation[]
    options?: Opt[]
}

export interface Opt {
    value: string | number
    desc: string
}

export interface Validation {
    type: 'required' | 'isEmail' | 'minLength' | 'isTrue' | 'oneOf'
    value?: string | number | boolean
    message: string
    ref?: string
}
Enter fullscreen mode Exit fullscreen mode

Also at once we create this type for the types of forms we are going to develop.
In this case we are only going to create two forms.

export type FormSection = 'register' | 'another'
Enter fullscreen mode Exit fullscreen mode

πŸ’Š Now we create the form object with the help of the typing.

Thanks to Typescript we can create our forms in this object.
We create a new folder src/lib and inside we create the file form.ts and add the following:

import { FormSection, InputProps } from '../types';

export const forms: { [K in FormSection]: InputProps[] } =
{

    register: [
        {
            label: "New username",
            type: "text",
            name: "username",
            placeholder: "New username",
            value: "",
            validations: [
                {
                    type: "minLength",
                    value: 3,
                    message: "Min. 3 characters",
                },
                {
                    type: "required",
                    message: "Username is required"
                },
            ],

        },
        {
            label: "New Password",
            type: "password",
            name: "password",
            placeholder: "New password",
            value: "",
            validations: [
                {
                    type: "required",
                    message: "Password is required"
                },
                {
                    type: "minLength",
                    value: 5,
                    message: "Min. 5 characters",
                }
            ],

        },
        {
            label: 'Repeat your password',
            type: "password",
            name: "repeat_password",
            placeholder: "Repeat password",
            value: "",
            validations: [
                {
                    type: "required",
                    message: "Repeat password is required"
                },
                {
                    type: "minLength",
                    value: 5,
                    message: "Min. 5 characters",
                },
                {
                    type: 'oneOf',
                    message: 'Passwords must match',
                    ref: 'password'
                }
            ],

        },

    ],

    another: [

        {
            label: "E-mail address",
            type: "email",
            name: "email",
            placeholder: "correo@correo.com",
            value: "",
            validations: [
                {
                    type: "required",
                    message: "Email is required"
                },
                {
                    type: "isEmail",
                    message: "Email no valid"
                }
            ],

        },
        {
            type: "select",
            name: "rol",
            label: "Select an option: ",
            value: "",
            options: [
                {
                    value: "admin",
                    desc: "Admin",
                },
                {
                    value: "user",
                    desc: "User"
                },
                {
                    value: "super-admin",
                    desc: "Super Admin"
                }
            ],
            validations: [
                {
                    type: "required",
                    message: "Rol is required"
                }
            ]
        },
        {
            type: "radio",
            name: "gender",
            label: "Gender: ",
            value: "",
            options: [
                {
                    value: 'man',
                    desc: "Man"
                },
                {

                    value: "woman",
                    desc: "Woman"
                },
                {

                    value: "other",
                    desc: "Other"
                },
            ],
            validations: [
                {
                    type: "required",
                    message: "Gender is required"
                }
            ]
        },
        {
            type: "checkbox",
            name: "terms",
            typeValue: "boolean",
            label: "Terms and Conditions",
            value: false,
            validations: [
                {
                    type: "isTrue",
                    message: "Accept the terms!"
                }
            ]
        },
    ]
}
Enter fullscreen mode Exit fullscreen mode

πŸ’Š Creating the validation schema for our form.

Let's create a new file in src/lib and name it getInputs.ts.
We create a new function to generate the validations to each input.
This function receives the fields, and each field is of type InputProps. We are also going to create 2 types only so that Typescript does not bother us later.

Note that we created the types YupBoolean and YupString. If you want you can add other types either to handle some other data type like numeric or array. For example:

  type YupNumber = Yup.NumberSchema<boolean | undefined, AnyObject, number | undefined>

I don't add it, because in my interfaces I don't handle any validation of type number or array.

import * as Yup from "yup";
import { AnyObject } from "yup/lib/types";
import { FormSection, InputProps } from '../types';
import { forms } from '../lib';

type YupBoolean = Yup.BooleanSchema<boolean | undefined, AnyObject, boolean | undefined>
type YupString = Yup.StringSchema<string | undefined, AnyObject, string | undefined>

const generateValidations = (field: InputProps) => {}
Enter fullscreen mode Exit fullscreen mode

First we create a variable that will be initialized with the datatype that will handle our input. The datatype we obtain it from the typeValue property, in case it is undefined by default the datatype will be string, and then we execute the function

let schema = Yup[field.typeValue || 'string']()
Enter fullscreen mode Exit fullscreen mode

Then we are going to go through the validations of the field, since it is an array.

Within the loop, we will use a switch case, evaluating what type of rule the field has.

const generateValidations = (field: InputProps) => {

    let schema = Yup[field.typeValue || 'string']()

    for (const rule of field.validations) {

        switch (rule.type) { }
    }
}
Enter fullscreen mode Exit fullscreen mode

In each case of the switch we will overwrite the schema variable. In the following way:

If it has an 'isTrue' validation it means that the input handles Boolean values, so we want our schema to behave as a YupBoolean, otherwise Typescript would be complaining. Then we execute the function that has to do with each case.

For example, in the case of 'isTrue', we execute the function with the exact same name, and inside we pass the message

case 'isTrue'   : schema = (schema as YupBoolean).isTrue(rule.message);  break;
Enter fullscreen mode Exit fullscreen mode

In the case that the validation is oneOf, we need to send it, as first parameter an array and as second parameter a message.

In the case of the array, it must be the value you want to match, but in this case we want to match the value of another field, so we use Yup.ref which needs a string that refers to the name attribute of an input.
So that when the validation is done, it checks if both fields contain the same value.

case 'oneOf'    : schema = (schema as YupString)
                                            .oneOf(
                                                    [ Yup.ref(rule.ref as string) ], 
                                                    rule.message
                                                  ); 
break;
Enter fullscreen mode Exit fullscreen mode

This is how our first function would look like. At the end we return the variable schema.
Note that at the beginning of the function, we place a condition where if the field has no validations then return null and avoid executing the cycle.

import * as Yup from "yup";
import { AnyObject } from "yup/lib/types";
import { FormSection, InputProps } from '../types';
import { forms } from '../lib';

type YupBoolean = Yup.BooleanSchema<boolean | undefined, AnyObject, boolean | undefined>
type YupString = Yup.StringSchema<string | undefined, AnyObject, string | undefined>

const generateValidations = (field: InputProps) => {

    if (!field.validations) return null

    let schema = Yup[field.typeValue || 'string']()

    for (const rule of field.validations) {
        switch (rule.type) {
            case 'isTrue'   : schema = (schema as YupBoolean).isTrue(rule.message);  break;
            case 'isEmail'  : schema = (schema as YupString).email(rule.message);  break;
            case 'minLength': schema = (schema as YupString).min(rule?.value as number, rule.message);  break;
            case 'oneOf'    : schema = (schema as YupString).oneOf([Yup.ref((rule as any).ref)], rule.message);  break;
            default         : schema = schema.required(rule.message);  break;
        }
    }

    return schema
}
Enter fullscreen mode Exit fullscreen mode

πŸ’Š Function to generate the inputs.

First we are going to create a function and we name it getInputs, which is of generic type and receives as parameter the section (that is to say that form you want to obtain its fields, in this case it can be the form of signUp or the other one).

We are going to create two variables that we will initialize them as empty objects and that at the end will have to contain new properties.


export const getInputs = <T>(section: FormSection) => {

    let initialValues: { [key: string]: any } = {};

    let validationsFields: { [key: string]: any } = {};

};
Enter fullscreen mode Exit fullscreen mode

Inside the function we will make a for of loop. In which we are going to go through the fields of a specific form.

  1. Inside the cycle, we are going to compute the values in the initialValues variable, and to compute the values we use the name property of the field.

  2. We verify if there are validations for the field.

    • If there are no validations, then continue with the next field.
    • If there are validations, we execute the function that we created before generateValidations sending the field as argument.
  3. Then to the validationsFields variable, we also compute the values using the name property of the field, and we assign the validation schema that has been generated.

for (const field of forms[section]) {

    initialValues[field.name] = field.value;

    if (!field.validations) continue;

    const schema = generateValidations(field)

    validationsFields[field.name] = schema;
}
Enter fullscreen mode Exit fullscreen mode

Once the cycle is finished, we must return 3 properties.

  • The validation schema inside a Yup.object, spreading the validationsFields properties.
validationSchema: Yup.object({ ...validationsFields }),
Enter fullscreen mode Exit fullscreen mode
  • The initial values, and we will make them behave as generic so that we can use them afterwards
initialValues: initialValues as T,
Enter fullscreen mode Exit fullscreen mode
  • The fields that we want to show in our form.
inputs: forms[section]
Enter fullscreen mode Exit fullscreen mode

This is what our function will look like at the end


export const getInputs = <T>(section: FormSection) => {

    let initialValues: { [key: string]: any } = {};

    let validationsFields: { [key: string]: any } = {};

    for (const field of forms[section]) {

        initialValues[field.name] = field.value;

        if (!field.validations) continue;

        const schema = generateValidations(field)

        validationsFields[field.name] = schema;
    }

    return {
        validationSchema: Yup.object({ ...validationsFields }),
        initialValues: initialValues as T,
        inputs: forms[section],
    };

};

Enter fullscreen mode Exit fullscreen mode

πŸ’Š Creating the form component.

First we are going to prepare the interface for the props that our Form component is going to receive.

  • onSubmit, function that executes the form.
  • labelButtonSubmit, text that will show the button.
  • titleForm, text that will show the form.

The last 3 properties are what returns the function that we did to generate the inputs and their validations.

interface Props {
    onSubmit: (data: unknown) => void
    labelButtonSubmit?: string
    titleForm?: string

    initialValues: unknown
    validationSchema: SchemaForm
    inputs: InputProps[]
}
Enter fullscreen mode Exit fullscreen mode

The validationSchema property is of type SchemaForm.

// src/types/index.ts
export type SchemaForm = OptionalObjectSchema<{
    [x: string]: any;
}, AnyObject, TypeOfShape<{
    [x: string]: any;
}>>
Enter fullscreen mode Exit fullscreen mode

Now we create the component, and inside we destructure the props that the component receives.

Then we use the hook of useForm, which we are going to establish an object as argument, we access to the property:

  • resolver, to set the validation scheme, for this we use the function yupResolver and we pass as argument the validationSchema that comes by props.
  • defaultValues, to establish the default values and we will assign the props of initialValues.

Note that we do not destructure anything of the useForm hook.

import { yupResolver } from '@hookform/resolvers/yup'
import { useForm } from 'react-hook-form'

export const Form = ({ ...props }: Props) => {
    const {
        initialValues,
        inputs,
        onSubmit,
        validationSchema,
        titleForm,
        labelButtonSubmit = 'Submit'
    } = props

    const formMethods = useForm({
        resolver: yupResolver(validationSchema),
        defaultValues: { ...(initialValues as any) }
    })

    return (
        <></>
    )
}
Enter fullscreen mode Exit fullscreen mode

Next, we are going to use a component that offers us react-hook-form, which is the FormProvider and we are going to spread the formMethods of the useForm hook.

The FormProvider will help us to communicate the state of the form with the components (inputs) that are nested inside the FormProvider. With the purpose of separating the components and not having everything in the same file.

Inside the FormProvider we will place a form and in the onSubmit method of the form label, we are going to execute a property of the formMethods, which is the handleSubmit, and as argument we pass the onSubmit that receives the component Form by props.

This handleSubmit will only be executed if there are no errors in each input, and when it is executed it will return the values of each input.

import { FormProvider, useForm } from 'react-hook-form'

// interface

export const Form = ({ ...props }: Props) => {
    // props

    const formMethods = useForm({
        resolver: yupResolver(validationSchema),
        defaultValues: { ...(initialValues as any) }
    })

    return (
        <FormProvider {...formMethods}>
            <form
                onSubmit={formMethods.handleSubmit(onSubmit)}
                className='bg-secondary rounded-md p-10 pt-5 shadow-2xl shadow-primary/30 flex flex-col gap-2 border border-primary w-full min-h-[390px]'
            >
                <section className='flex-1 flex flex-col gap-3'>
                    {/* inputs here */}
                </section>
            </form>
        </FormProvider>
    )
}
Enter fullscreen mode Exit fullscreen mode

Now we are going to create a function, to return the different types of inputs.
We use the prop inputs that we destructured of the props that the Form component receives.

Based on the type of input we are going to render one or another input.

Note that we are using components that we have not yet created. Also note, that from the properties of each input, we are going to exclude the validations, typeValue and value, because they are values that our input does not need directly.

One thing to improve about this function, is that you can create a separate component, and create a dictionary with the components and the type of input.
In this case I do not do it, so as not to extend more.

const createInputs = () =>
    inputs.map(({ validations, typeValue, value, ...inputProps }) => {

        switch (inputProps.type) {
            case 'select':
                return <CustomSelect {...inputProps} key={inputProps.name} />
            case 'checkbox':
                return <CustomCheckbox {...inputProps} key={inputProps.name} />
            case 'radio':
                return <CustomRadio {...inputProps} key={inputProps.name} />
            default:
                return <CustomInput {...inputProps} key={inputProps.name} />
        }
    })

Enter fullscreen mode Exit fullscreen mode

Finally, we execute the createInputs function inside the section tag. And immediately we are going to create the custom inputs.

// imports 

// interface
export const Form = ({ ...props }: Props) => {
    // props

    const formMethods = useForm({
        resolver: yupResolver(validationSchema),
        defaultValues: { ...(initialValues as any) }
    })

    const createInputs = () =>
        inputs.map(({ validations, typeValue, value, ...inputProps }) => {
            switch (inputProps.type) {
                case 'select':
                    return <CustomSelect {...inputProps} key={inputProps.name} />
                case 'checkbox':
                    return <CustomCheckbox {...inputProps} key={inputProps.name} />
                case 'radio':
                    return <CustomRadio {...inputProps} key={inputProps.name} />
                default:
                    return <CustomInput {...inputProps} key={inputProps.name} />
            }
        })

    return (
        <FormProvider {...formMethods}>
            <form
                onSubmit={formMethods.handleSubmit(onSubmit)}
                className='bg-secondary rounded-md p-10 pt-5 shadow-2xl shadow-primary/30 flex flex-col gap-2 border border-primary w-full min-h-[390px]'
            >
                <section className='flex-1 flex flex-col gap-3'>
                    { createInputs() }
                </section>

            </form>
        </FormProvider>
    )
}
Enter fullscreen mode Exit fullscreen mode

πŸ’Š Creating the components of each input.

First, we are going to create an error message, which is going to be displayed every time the input validation fails.

Inside src/components we create ErrorMessage.tsx.

interface Props { error?: string }

export const ErrorMessage = ({ error }: Props) => {
    if (!error) return null

    return (
        <div className='w-full grid place-content-end'>
            <p className='text-red-400 text-sm'>{error}</p>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Now, we are going to create a new folder src/components/inputs and inside we will create 4 files.

These four components that we are going to create receive props that are of type CustomInputProps. You can place it in the src/types/index.ts file.

export type CustomInputProps = Omit<InputProps, 'validations' | 'typeValue' | 'value'>

And also as each input that we will create will be inside a FormProvider, we can use another custom hook of react-hook-form, which is useFormContext, this hook will help us to connect the state of the form with the input.

  1. CustomGenericInput.tsx

From useFormContext, we obtain the register property, and the errors property inside the formState.

const {
        register,
        formState: { errors }
    } = useFormContext()
Enter fullscreen mode Exit fullscreen mode

We create the error, computing the error object with the prop name that the component receives and we obtain the message.

const error = errors[name]?.message as string | undefined
Enter fullscreen mode Exit fullscreen mode

At the moment of constructing the input, we need to spread the properties of the register function, which we have to pass the prop name so that react-hook-form identifies what errors and validations this input should have.
Then, we spread the other properties in case it has more (such as the placeholder).

<input
    className='py-1 px-2 rounded w-full text-black'
    {...register(name)}
    {...props}
    id={id}
/>
Enter fullscreen mode Exit fullscreen mode

This is how this component will look like in the end.

import { useFormContext } from 'react-hook-form'
import { ErrorMessage } from '../../components'
import { CustomInputProps } from '../../types'

export const CustomInput = ({ name, label, ...props }: CustomInputProps) => {
    const {
        register,
        formState: { errors }
    } = useFormContext()

    const error = errors[name]?.message as string | undefined

    const id = `${name}-${props.type}-${label}`

    return (
        <div className='w-full flex gap-1 flex-col'>
            {label && (
                <label className='text-white text-sm' htmlFor={id}>
                    {label}
                </label>
            )}

            <input
                className='py-1 px-2 rounded w-full text-black'
                {...register(name)}
                {...props}
                id={id}
            />

            <ErrorMessage error={error} />
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode
  1. CustomCheckbox.tsx
import { useFormContext } from 'react-hook-form'
import { ErrorMessage } from '../../components'
import { CustomInputProps } from '../../types'

export const CustomCheckbox = ({ name, label, ...props }: CustomInputProps) => {
    const {
        register,
        formState: { errors }
    } = useFormContext()

    const error = errors[name]?.message as string | undefined

    return (
        <div>
            <label className='flex gap-2 items-center cursor-pointer w-fit'>
                <input {...props} {...register(name)} />
                {label}
            </label>

            <ErrorMessage error={error} />
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode
  1. CustomSelect.tsx

This input is almost the same as all the others, only here we have the prop options where the values of the select that can be selected will come.

import { useFormContext } from 'react-hook-form'
import { ErrorMessage } from '../../components'
import { CustomInputProps } from '../../types'

export const CustomSelect = ({ name, label, options, ...props }: CustomInputProps) => {
    const {
        register,
        formState: { errors }
    } = useFormContext()

    const error = errors[name]?.message as string | undefined
    const id = `${name}-${props.type}-${label}`

    return (
        <div className='flex flex-col gap-2'>
            <div className='flex items-center gap-4'>
                <label htmlFor={id}>{label}</label>

                <select {...register(name)} {...props} id={id} className='p-2 rounded flex-1 text-black'>

                    <option value=''>--- Select option ---</option>

                    {options &&
                        options.map(({ desc, value }) => (
                            <option key={value} value={value}>
                                {desc}
                            </option>
                    ))}

                </select>

            </div>
            <ErrorMessage error={error} />
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode
  1. CustomRadioGroup

Very similar to CustomSelect.tsx. Only here we render an input of type radio.

import { useFormContext } from 'react-hook-form'
import { ErrorMessage } from '../../components'
import { CustomInputProps } from '../../types'

export const CustomRadio = ({ name, label, options, ...props }: CustomInputProps) => {
    const {
        register,
        formState: { errors }
    } = useFormContext()

    const error = errors[name]?.message as string | undefined

    return (
        <div className='flex flex-col'>
            <div className='flex items-center gap-4'>
                <label>{label}</label>

                <section className='flex justify-between flex-1'>
                    {options &&
                        options.map(({ desc, value }) => (

                            <label
                                key={value}
                                className='flex items-center gap-1 cursor-pointer hover:underline rounded p-1'
                            >
                                <input {...register(name)} {...props} value={value} type='radio' />
                                {desc}
                            </label>

                        ))}
                </section>
            </div>
            <ErrorMessage error={error} />
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

πŸ’Š Using our Form component.

Now we go to the src/App.tsx file.

To use the Form component.

We have to execute the getInputs function and get the validations, initial values and inputs. We will do it outside the component. We also create an interface so that the initial values behave like that interface.

interface SignUpFormType {
    username: string
    password: string
    repeat_password: string
}

const signUpForm = getInputs<SignUpFormType>('register')
Enter fullscreen mode Exit fullscreen mode

Then we import the Form component, we spread the properties returned by getInput. And we also pass it the other props.

import { Layout, Form } from './components'
import { getInputs } from './lib'

interface SignUpFormType {
    username: string
    password: string
    repeat_password: string
}


const signUpForm = getInputs<SignUpFormType>('register')

const App = () => {

    const onSubmitSignUp = (data: unknown) => console.log({ singUp: data })

    return (
        <Layout>
            <Form
                {...signUpForm}
                onSubmit={onSubmitSignUp}
                titleForm='Sign Up!'
                labelButtonSubmit='Create account'
            />
        </Layout>
    )
}
export default App
Enter fullscreen mode Exit fullscreen mode

In case you want to overwrite the initial values, you just create a new constant by spreading the initial values and then overwriting what you need. Then pass a new value to the initialValues prop.


const App = () => {
    const onSubmitSignUp = (data: unknown) => console.log({ singUp: data })

    const initialValuesSignUp: SignUpFormType = {
        ...signUpForm.initialValues,
        username: '@franklin361'
    }

    return (
        <Layout>
            <Form
                {...signUpForm}

                initialValues={initialValuesSignUp}

                onSubmit={onSubmitSignUp}
                titleForm='Sign Up!'
                labelButtonSubmit='Create account'
            />
        </Layout>
    )
}
export default App
Enter fullscreen mode Exit fullscreen mode

And you can also include several forms dynamically.

import { Layout, Form } from './components'
import { getInputs } from './lib'

interface SignUpFormType {
    username: string
    password: string
    repeat_password: string
}

interface AnotherFormType {}

const signUpForm = getInputs<SignUpFormType>('register')
const anotherForm = getInputs<AnotherFormType>('another')

const App = () => {
    const onSubmitSignUp = (data: unknown) => console.log({ singUp: data })

    const onSubmitAnotherForm = (data: unknown) => console.log({ another: data })

    const initialValuesSignUp: SignUpFormType = {
        ...signUpForm.initialValues,
        username: '@franklin361'
    }

    return (
        <Layout>
            <Form
                {...signUpForm}
                initialValues={initialValuesSignUp}
                titleForm='Sign Up!'
                onSubmit={onSubmitSignUp}
                labelButtonSubmit='Create account'
            />

            <Form
                {...anotherForm}
                titleForm='Another form!'
                onSubmit={onSubmitAnotherForm}
                labelButtonSubmit='Send info'
            />
        </Layout>
    )
}
export default App
Enter fullscreen mode Exit fullscreen mode

cover

πŸ’Š Conclusion.

React Hook Form is one of my favorite libraries, because it has certain advantages over other popular libraries such as Formik; for example the bundle size is smaller, it has fewer dependencies, produces fewer re-renders, etc. πŸ˜‰.

But still both are very used libraries.

I hope you liked this post and I also hope I helped you to understand how to make dynamic forms using React Hook Form πŸ™Œ.

If you know any other different or better way to make this application feel free to comment it.

I invite you to review my portfolio in case you are interested in contacting me for a project.! Franklin Martinez Lucas

πŸ”΅ Don't forget to follow me also on twitter: @Frankomtz361

πŸ’Š Demo.

https://dynamic-form-rhf.netlify.app/

πŸ’Š Source code.

https://github.com/Franklin361/dynamic-form-rhf/

Top comments (6)

Collapse
 
flash010603 profile image
Usuario163

Excellent post, and just what I needed, I will use it in my next project

Collapse
 
geekyayush profile image
Ayush Somani

This is what I call a perfect tutorial. Thank you.

Collapse
 
dolygd profile image
Doly Tiwari

Wow, I am new to React and wanted to implement dynamic form using react-hook-form and yup. Thank you so much for explaining in detail.

Collapse
 
phd45 profile image
Phil45

Very interesting post, thank you very much ! Do you think this pattern can be easily implemented with Zod ?

Collapse
 
matija2209 profile image
matija2209

How come there is no use of Controller same control?

Collapse
 
bluebill1049 profile image
Bill

Thanks for sharing this post!