Three ways to build React forms– Part 2
Custom validation with a Controlled Component
Part of the
Three ways to build React forms
Series
In Part 1 we talked about building a simple contact form using an uncontrolled component. In an uncontrolled component, validation is left up to the browser. As we discussed, this can lead to an inconsistent experience across browsers.
In this tutorial we’ll modify our basic form by taking control of the form’s state. In doing this we’ll create what React calls a “controlled component.” With a controlled component we’ll have more control over the form, allowing us to create a consistent validation experience, and customize it however we like.
Once again we’re working with Formspree on the backend, but the principles should still apply if you’re using your own form backend.
Building a Controlled Component Form
To create a controlled component, we need to add handlers for each input event and take control of the inputs’ states. Below I’ve taken the original uncontrolled form from Part 1 and reimplemented it as a controlled component. The new changes are highlighted.
|
|
Notes:
- On line 6, a new state variable
inputs
has been introduced to capture the state of the form. It’s an object with a key for every input field. We’ve also added ahandleOnChange
function that we can call to update theinputs
state. - On line 62 and 63 an input is bound to it’s state through the
onChange
andvalue
attributes. - Everything else is the same as the basic form in Part 1.
At this point we have a controlled component and we haven’t added that much code. However it doesn’t offer any meaningful improvements over our form in Part 1 — at least not yet. To see the value of controlled components, we need to add our own validation.
Adding custom validation
With custom validation we can define our own rules and control the rendering of error states. We’ll discuss the benefits and drawbacks in more depth at the end, but first let’s modify our form.
To add validation we need 4 things:
- A way to store field error states
- Validation rules for each input
- A
validate
function that will check each input and update the validation state - Some way to display the validation results
Here’s the new validation code that meets the four requirements above. I’ve broken down into 4 steps. Each block of code will be inserted at the top of MyForm
.
// Step 1. create new state to track field errors
const [fieldErrors, setFieldErrors] = useState({});
// Step 2. add validation rules for each input field
const validationRules = {
email: val => val && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),
message: val => !!val // similar to "required"
};
// Step 3. create a validate function that updates state, and returns
// true if all rules pass
const validate = () => {
let errors = {};
let hasErrors = false;
for (let key of Object.keys(inputs)) {
errors[key] = !validationRules[key](inputs[key]);
hasErrors |= errors[key];
}
setFieldErrors(prev => ({ ...prev, ...errors }));
return !hasErrors;
};
// Step 4. add a render method to display field errors
const renderFieldError = field => {
if (fieldErrors[field]) {
return <p className="errorMsg">Please enter a valid {field}.</p>;
}
};
We’re not done yet. Now we need to call the validate function when the user clicks submit
. If the validation fails we shouldn’t send the form submission.
const handleOnSubmit = event => {
event.preventDefault();
if (!validate()) {
return;
}
/* ... rest of handleOnSubmit... */
Then, in our main render method, we can call renderFieldError
for each input. For example, just after the email
input field, we can add this line:
{renderFieldError("email")}
We also want to disable default validation on the form. That can be accomplished by adding the noValidate
attribute:
<form onSubmit={handleOnSubmit} noValidate>
If the form is submitted successfully, we should clear any field errors in handleServerResponse
:
const handleServerResponse = (ok, msg) => {
// ...
if (ok) {
setFieldErrors({}); // <-- clear field errors
setInputs({
email: "",
message: ""
});
}
};
Finally, it would be cool if after the first submit, we perform input validation for each keystroke. That way the user gets fast feedback while correcting field errors. We can do that with a React useEffect
hook that’s called each time the inputs change.
useEffect(() => {
// Only perform interactive validation after submit
if (Object.keys(fieldErrors).length > 0) {
validate();
}
}, [inputs]);
Note: we only call validation if there are already
fieldErrors
. Otherwise the form will display errors even before the user has had a chance to complete the form.
Here’s how the form looks now that we’ve got some basic validation set up:
And here’s the full form code with the new validation logic highlighted:
|
|
Benefits and Drawbacks
Implementing a custom controlled form has several benefits over the uncontrolled form.
- We can define our own validation logic. This ensures that the form will consistently produce the data we expect, regardless of the browser.
- State is managed by the form component. That means we can create a custom validation experience, and know that it will render and behave consistently across browsers.
- The form is able to take full advantage of React’s event and rendering loop. This lets us respond to each change and perform validation per keystroke, rather than at the end when the user clicks
submit
.
However there are some pretty major drawbacks as well:
- We need to add event handling code for every input field. This gets pretty complex quickly.
- Most event handling code is similar, resulting in a lot of boilerplate code.
- Validation rules are manual. To achieve the same breadth of validation functionality provided by browsers, we’d need to implement a lot of rules.
For simple forms, rolling our own event handling and validation is manageable. But after 3 or 4 inputs, the form code may become a burden. Instead, we can use a form library that comes with this functionality built-in. In Part 3 we’ll be recreating our basic form with the a popular form library Formik. We’ll see how it helps us maintain the control and consistency of a controlled component form without quite as much boilerplate code.