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 .
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.
export type FormSection = 'register' | 'another'
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:
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) => {}
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']()
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) { } }}
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;
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;
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}
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 } = {};};
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.
for (const field of forms[section]) { initialValues[field.name] = field.value; if (!field.validations) continue; const schema = generateValidations(field) validationsFields[field.name] = schema;}
Once the cycle is finished, we must return 3 properties.
The validation schema inside a Yup.object , spreading the validationsFields properties.
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.
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.
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.
const error = errors[name]?.message as string | undefined
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 ).
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.
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.
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 π.