Client Side Form Validation in React


Updated Jun 27th, 2023

Immediately versus After Delay versus On Submit

Not so much a React thing but very important. Some warnings need to be shown right away, (too many characters in a field). Others need to be shown after a delay, (when the user has finished typing a username check and see if it already exists, is a valid email address, or make sure there is a minimum number of characters).

There are other client side warnings that are checked on submit, like if the field is left blank, (more on this below).


It seems wordy or verbose but I think this may have to do with how surprisingly complicated client side validation for a form with many inputs can be. What are the better solutions?

Watch Unexpected Logic

The way to watch for unexpected logic is to render a table with the expected value, hasErrors, and message values. Bugs arise when a field gets an error based on the value of another field, and although that other field’s value may change (clearing that field’s error) the original field’s error is never reset because it is never typed in.

For example, there is a “field_one_range_high” that needs to be greater than “field_two_range_low.” If the user submits the form with a “field_two_range_low” that is less than the “field_one_range_high” there should be an error added alerting the user and preventing the form submittal. But if the problem is fixed by changing the “field_one_range_high” value instead of the “field_two” value then “field-two’s” value is never cleared.

This check may be better suited for an “on submit” check with a form wide warning or flash message versus an input-based warning message.

You could also clear all errors on form submit and then re-run all of the checks but this may not be desired.

You could also have a separate piece of state. This is a valid approach.

Another Example: Only if a signal is triggered do we need a value for the time triggered and whether the signal was a success or not. If a user submits the form and there is an empty signal but the result is added then we need to show a warning. But the error can be resolved by either, adding a signal value, or by removing the result/dependency value.

Another approach would be to only conditionally show the dependent fields. If the signal is not selected because there was no signal triggered then don’t even show the other fields.

onBlur versus onChange versus onSubmit

When leveraging useReducer, the onChange is absolutely necessary for updating state with every keystroke for immediate checks like too many characters.

When it comes to checking if a field is blank you can use an onBlur but you end up with an undesired result. When the page loads there is no error but if you changes pages the error may flash briefly. Also, if you click into the field and then click out, whether you every type into the field or not the error will show. Alternatively you may want to have the “field cannot be left empty” warnings “onSubmit” only. This approach avoids having a user type in a value, say too long or short and even after clearing the value the error persists.

If you show an error when a field is left blank but before submit then if a user types in a value but then clears the field, the error will stay and this is not what we want.

How Do You Clear the Fields?

Using a case like “clearFields” and setting “draft.whateverField.value to an empty string does not change the front end. So how do we update the field?

You should have two way binding on the form input, meaning there is a “value” prop set to the state and an “onChange” that updates the state.

            onChange={e => {
              dispatch({ type: "incorrectAnswer3Immediately", value: })

Avoid One of My Pet Peeves

Something is wrong with the form so it clears the form requiring you to start over. You don’t see this much anymore, thank goodness, but you’ve likely experience it before.

Warning Times Out

If you set a timer to let the warning timeout you end up with more code because you need to make those checks again when the form is submitted. I still like this though. You can accomplish this in a “setTimeout” inside a “useEffect,” don’t forget the teardown function. Not sure if it is better to have multiple “useEffect” hooks, or multiple items in the dependency array.

  useEffect(() => {
    if (state.description.hasErrors || state.price.hasErrors || state.miles.hasErrors) {
      const timer = setTimeout(() => {
        dispatch({ type: "removeAnyErrors" })
      }, 3000)
      return () => {
  }, [state.description.hasErrors, state.price.hasErrors, state.miles.hasErrors])


Auto complete can be annoying so you may want to set “autoComplete” to off on the input itself.

Speaking of “autoComplete,” don’t forget “autoFocus.” This is not so much a validation thing but good form UX.

textarea has little “expand” handle in the bottom right of the textarea by default. This can be removed but it’s done in the CSS.

textarea has a “rows” option but it’s value is not in quotes but in curly brackets.

Don’t forget your “name” and “aria-label” attributes on the inputs and the “htmlFor” on the labels.

Likely want to trim values when checking if a field is left blank to avoid spaces from clearing your check.