Easily Validate React Forms using react-validatable-form

Ahmet Emre Kılınç
Codable
Published in
6 min readOct 4, 2022

Validating large forms is a complex challenge in front-end projects. There will probably be many things to consider (and handle), like:

  • Validating values, immediately after every value change.
  • Hiding validation results before the form is submitted.
  • Displaying the validation error when the element looses focus.
  • Conditional validation of fields.
  • Validating each element of a list using the same rule.
  • Building custom reusable rules (to be used anywhere in the app).
  • Localization and customization of error messages for the same validation rules.
  • Focusing on the first invalid element (one with a validation error), following form submission.

In react projects, all these requirements can easily be managed via the react-validatable-form library.

In order to use react-validatable-form in your project, you should first, add it to the project.

If you are using npm, run:

npm install react-validatable-form

If you are using yarn, run:

yarn add react-validatable-form

After that, you need to wrap your app with ReactValidatableFormProvider:

import { ReactValidatableFormProvider } from 'react-validatable-form';//....
return <ReactValidatableFormProvider>
<App />
</ReactValidatableFormProvider>;

Next, you can use useValidatableForm hook anywhere in your app:

import { useValidatableForm } from 'react-validatable-form';
import TextField from '@mui/material/TextField';
//....

const initialFormData = {};
const rules = [{ path: 'val', ruleSet: [{ rule: 'required' }] }];
//....const { isValid, formData, setPathValue, getValue, getError } = useValidatableForm({
rules,
initialFormData,
});
return <TextField
error={!!getError('val')}
helperText={getError('val') || ' '}

type="text"
value={getValue('val') || ''}
onChange={(e) => setPathValue('val', e.target.value)}
/>;

The output of this code on initial render is:

rvf-1

useValidatableForm is a React hook that manages the validations of input elements in a form. In this example, it takes 2 parameters:

  • rules: an array that contains the rules to be checked on the form,
  • initialFormData: the default values of the form datum, preset in the initial rendering process.

In this example, it returns 5 parameters:

  • isValid: boolean flag, returning whether the form is valid or not at that moment,
  • formData: form data that contains values of all paths in the form,
  • setPathValue: function to update the value of the given path,
  • getValue: retrieves the current value of the given path,
  • getError: retrieves the current error message of the given path.

Since end users generally prefer not to see any errors until the form is submitted, it is better to hide them, before. For this, we can set hideBeforeSubmit prop to true;

either in useValidatableForm (form-scoped):

useValidatableForm({
rules,
initialFormData,
hideBeforeSubmit: true
});

or in ReactValidatableFormProvider (app-scoped):

<ReactValidatableFormProvider hideBeforeSubmit={true}>
<App />
</ReactValidatableFormProvider>

Similarly, end users may want to see the errors when they focus on a component. For this, we can set showAfterBlur prop to true;

either in useValidatableForm (form-scoped):

useValidatableForm({
rules,
initialFormData,
hideBeforeSubmit: true,
showAfterBlur: true
});

or in ReactValidatableFormProvider (app-scoped):

<ReactValidatableFormProvider hideBeforeSubmit={true} showAfterBlur={true}>
<App />
</ReactValidatableFormProvider>

For enhanced user experience, focusing on the first error after the form is submitted, can be an indispensable feature. For this, we can set focusToErrorAfterSubmit prop to true;

either in useValidatableForm (form-scoped):

useValidatableForm({
rules,
initialFormData,
hideBeforeSubmit: true,
showAfterBlur: true,
focusToErrorAfterSubmit: true
});

or in ReactValidatableFormProvider (app-scoped):

<ReactValidatableFormProvider hideBeforeSubmit={true} showAfterBlur={true} focusToErrorAfterSubmit={true}>
<App />
</ReactValidatableFormProvider>

Note that we need to set the id attribute of the elements to be the same as the value of the path property of the corresponding rules, otherwise the hook can fail to find the element in DOM.

Beyond that, in most cases, we need to consider HTML components that are not focusable (has no input tag to be focused on), such as checkbox, radio button, table, … etc. For this, we can use elementFocusHandler prop;

either in useValidatableForm (form-scoped):

const myCustomElementFocusHandler = (elementId) => {
const element = document.getElementById(elementId);
if (!element) {
console.warn(`Dom element with id ${elementId} could not be found`);
} else {
const parentElement = element.parentElement;
const grantParentElement = parentElement.parentElement;
grantParentElement.classList.add('element-shaking');
element.focus();
setTimeout(() => {
grantParentElement.classList.remove('element-shaking');
}, 600);
}
};
useValidatableForm({
rules,
initialFormData,
hideBeforeSubmit: true,
showAfterBlur: true,
focusToErrorAfterSubmit: true,
elementFocusHandler: myCustomElementFocusHandler
});

or in ReactValidatableFormProvider (app-scoped):

const myCustomElementFocusHandler = (elementId) => {
const element = document.getElementById(elementId);
if (!element) {
console.warn(`Dom element with id ${elementId} could not be found`);
} else {
const parentElement = element.parentElement;
const grantParentElement = parentElement.parentElement;
grantParentElement.classList.add('element-shaking');
element.focus();
setTimeout(() => {
grantParentElement.classList.remove('element-shaking');
}, 600);
}
};
<ReactValidatableFormProvider hideBeforeSubmit={true} showAfterBlur={true} focusToErrorAfterSubmit={true} elementFocusHandler={myCustomElementFocusHandler}>
<App />
</ReactValidatableFormProvider>

This code shows an example of visualizing the form validation by adding a temporary shaking effect (using CSS) to the element with the first validation error, after the form is submitted.

This screenshot is an example output of this code:

Custom elementFocusHandler with shaking effect

The default rules defined in react-validatable-form are:

  • required: checks if the given value is not one of the values: undefined, null, empty string, or empty array
  • number: checks if the given value is a valid number
  • length: checks the length of a string
  • listSize: checks the length of an array
  • date: checks if the given value is a valid date
  • email: checks if the given value is a valid email
  • url: checks if the given value is a valid URL
  • iban: checks if the given value is a valid IBAN
  • equality: checks if the given value is equal to the comparison value
  • includes: checks if the given value includes the comparison value
  • regex: checks if the given value is a valid string matching the given regex
  • unique: rule checks if non-unique values exist on a listPath

Other than these rules, any custom rule can be defined inside of a component, as a function:

const customRule = (ruleParams) => {
const { value } = ruleParams;
if (value && (!value.includes('g') || value.length < 5)) {
return 'this field should include letter `g` and its length should be greater than 5';
}
return null;
};
const rules = [{ path: 'val', ruleSet: ['required', { rule: customRule }] }];

A non-empty return value for a custom rule means a validation error.

Similarly, custom rules can also be made app-scoped by passing them to the ReactValidatableFormProvider via customRules prop with a unique rule name:

const MyCustomRuleFunction = (ruleParams) => {
const { value } = ruleParams;
if (!value) {
return 'this field is a required field';
}
if (!value.includes('a') && value.length < 5) {
return `text ${value} should either include letter 'a' or its length should be greater than 4`;
}
return null;
};
const customRules = {
myCustomRule: MyCustomRuleFunction
};
<ReactValidatableFormProvider customRules={customRules}>
<App />
</ReactValidatableFormProvider>

In the above example, myCustomRule rule can be used anywhere in the app, just like the default rules; required, number, … etc.

By default, validation rules are run whenever there is a change in the corresponding path. If the rule depends on a condition, we can use enableIf & disableIf parameters in the rule definition:

const rules = [
{
path: 'val1',
ruleSet: [
{
rule: 'required',
disableIf: (formData) => formData['disableVal1Rule'],
},
],
dependantPaths: ['disableVal1Rule'],
},
{
path: 'val2',
ruleSet: [
{
rule: 'required',
enableIf: (formData) => formData['enableVal2Rule'],
},
],
dependantPaths: ['enableVal2Rule'],
},
];

We need to use dependantPaths parameter to force the rule to run whenever there is a change on the paths other than the original path.

dependantPaths parameter is also required, if the compared value is not static, which means that it is a dynamic value depending on another value in the formData:

const rules = [
{
path: 'val',
ruleSet: [
{
rule: 'number',
greaterThan: (formData) => formData['comparisonValue']
}
],
dependantPaths: ['comparisonValue']
}
];

For more information and live examples, you can visit react-validatable-form-demo (for various customizations of react-validatable-form library).

You can also try a live example on StackBlitz.

--

--