Reputation: 167
Whats the best way to perform form validations in React. Can't we just use HTML5 validations such as a required field or an email field validations. For complex validation what the best way to do with React
Upvotes: 2
Views: 1039
Reputation: 2686
HTML 5 validation is too limited and can easily be manipulated. Hence client-side (using JS for better experience) and server-side validation (for security purposes)
I believe this is the most annoying and time-consuming part of writing a React application so created myself a small react hook to simplify the job for me. It uses validator under the hood and returns event handlers as well as the field values. If you are planning to use this method, please read the code carefully to understand what's going on. Sounds simple but there are a few concepts you need to understand.
At first, I decided the shape of the input for the hook which looks like so:
fieldName: {
value: '', // Initial state
validationRules: [
{
method: funcToExec, // The validation function to execute
validWhen: false,
params: [param1, param2], // The parameters to pass down to the function
errorText: 'This field should not be left empty!', // The error to prompt the user
},
{
method: equals,
compareTo: "anotherFieldName", // Used for cross field validation
errorText: 'This field should have length between 6 and 20 characters!',
},
],
},
My validation strategy was as follow:
Debouncing is also essential as it improves the user interface.
With these ideas being listed out, I started off by writing a transformer that transforms the input into the initial state for the useState()
hook as follow:
const initialFieldState = {};
for (const key of Object.keys(fields))
initialFieldState[key] = {
value: fields[key].value,
isValid: true, // Initial state has to be true as we assume that the user has touched the field yet
errorText: null,
};
After that, I wrote this small validation function:
const validate = useCallback((valueToValidate, rules) => {
for (let i = 0; i < rules.length; i += 1) {
const { method, params = [], validWhen = true, errorText } = rules[i];
if (method(valueToValidate, ...params) !== validWhen) return [false, errorText];
}
return [true, null]; // Destructured as [isValid, errorText]
}, []);
My next step was to create 3 event handlers: onChange
, onFocus
, onBlur
. But before doing that I realised I needed a debounced function that calls validate
to perform validation and set the field states. So I made this, quite intuitive:
const validateField = useCallback(
debounce((name, value, callback) => {
const currentRules = fields[name].validationRules;
const enhancedRules = enhanceValidationRules(currentRules);
const [isValid, errorText] = validate(value, enhancedRules);
setFieldState(prevState => ({
...prevState,
[name]: {
...prevState[name],
isValid,
errorText,
},
}));
if (callback != null) callback(isValid); // Don't worry about this callback, I'll explain later
}, 2000),
[],
);
Which calls enhanceValidationRules
which basically just manipulates the validationRules
slightly for cross field validation like so:
const enhanceValidationRules = useCallback(rules => {
// Split the rules into 2 parts: The ones with cross field validations and the ones without
const [crossFieldRules, otherRules] = partition(rules, ({ compareTo }) => !!compareTo);
// Manually add values of other fields as params
const enhancedCrossFieldRules = crossFieldRules.map(rule => ({
...rule,
params: [latestFieldState.current[rule.compareTo].value],
}));
// Merge them again
return [...otherRules, ...enhancedCrossFieldRules];
}, []);
Notice latestFieldState
is a ref used to store the latest state of the fields as setState()
is async in React. Further reference
After this, the event handlers are pieces of cake:
const handleFieldChange = useCallback(
e => {
const { name, value } = e.target;
setFieldState(prevState => ({
...prevState,
[name]: {
...prevState[name],
value,
},
}));
validateField(name, value);
},
[validateField],
);
const handleFieldBlur = useCallback(
e => {
const { name, value } = e.target;
validateField(name, value);
// Should validate immediately when user unfocuses the field
validateField.flush();
},
[validateField],
);
const handleFieldFocus = useCallback(
e => {
const { name, value } = e.target;
setFieldState(prevState => ({
...prevState,
[name]: {
...prevState[name],
errorText: null,
},
}));
// Validate after the user focuses on the field
validateField(name, value);
},
[validateField],
);
Obviously, you need to clear out the debounced function as well, like so:
useEffect(() => () => validateField.cancel(), [validateField]);
Okay, ALMOST THERE!
You might want to validate the whole form on more time after submission, but validateField
doesn't return the value immediately. flush
doesn't work either as it doesn't allow us to assign values to another variable. This is when callback comes into play. So we can implement it like so:
const validateFieldAfterSubmit = useCallback(
(name, value) => {
let pass;
validateField(name, value, isValid => {
pass = isValid;
});
validateField.flush();
return pass;
},
[validateField],
);
That's it, you got it! The code is posted here as well as how you can use it. I used firebase and material-ui to make a full blown login box to demonstrate how useful and clean the hook could be.
Please feel free to ask me questions or report any bugs.
Upvotes: 2
Reputation: 1776
You can use HTML5 validations, here is article about that.
Here are two more resources for form validations in react:
Upvotes: 0