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. π
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).
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.
Then we install the dependencies.
Then we open the project in a code editor (in my case VS code).
First steps.
Inside the src/App.tsx file we delete everything and create a component that displays a hello world.
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 .
Now, inside the src/App.tsx file, we add the layout.
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.
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.
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.
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:
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:
I donβt add it, because in my interfaces I donβt handle any validation of type number or array.
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
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.
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
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.
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.
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.
Inside the function we will make a for of loop. In which we are going to go through the fields of a specific form.
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.
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.
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.
Once the cycle is finished, we must return 3 properties.
The validation schema inside a Yup.object , spreading the validationsFields properties.
The initial values, and we will make them behave as generic so that we can use them afterwards
The fields that we want to show in our form.
This is what our function will look like at the end
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.
The validationSchema property is of type SchemaForm .
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.
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.
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.
Finally, we execute the createInputs function inside the section tag. And immediately we are going to create the custom inputs.
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 .
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.
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.
CustomGenericInput.tsx
From useFormContext , we obtain the register property, and the errors property inside the formState.
We create the error, computing the error object with the prop name that the component receives and we obtain the message.
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 ).
This is how this component will look like in the end.
CustomCheckbox.tsx
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.
CustomRadioGroup
Very similar to CustomSelect.tsx. Only here we render an input of type radio.
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.
Then we import the Form component, we spread the properties returned by getInput . And we also pass it the other props.
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.
And you can also include several forms dynamically.
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 π.