Validating React form in componentDidUpdate()

Viewed 463

I have a form that uses immense amount of logic with auto-filling fields, depending on user input etc. In addition, I need to provide live validation while user is typing.

I used to call validators manually, depending on what fields setState() would update, but very soon that became unmaintainable. I then created a function that calls all validator methods I have, and manually invoke it every time I update the state.

Now I think about going further, and automate invoking that function. I want to implement this using componentDidUpdate() react lifecycle method.

componentDidUpdate(prevProps, prevState) {  
  if (_.isEqual(prevState.data, this.state.data))
    return;
  this.validateAll();
}

Now, if any of the form field changed - we run full validation to be sure, that we didn't forget anything. The problem though is that lodash _.isEqual() is a costly deep-comparision operation, and componentDidUpdate() lifecycle method is invoked on any user input, so this can potentially lead to performance issues.

I decided to reduce amount of that comparisons as well as amount of validation function invocations by debouncing.

handleValidation = (prevState) => {
  if (_.isEqual(prevState.data, this.state.data))
    return;
  this.validateAll();
};

debouncedHandleValidation = _.debounce(this.handleValidation, 200);

componentDidUpdate(prevProps, prevState) {
  this.debouncedHandleValidation(prevState);
}

Here is a Live demo (calls to deep compare and validation are logged)

My question is: is it okay to handle form validation this way, or there is something I didn't take into account and my approach is bad?

Thanks.

1 Answers

The most common pattern for handling validation of forms in react is to check if the input is valid in its onChange props.

One of the reason is fairly obvious, you'll want to set both validation errors and value simultaneously.

Another obvious reason is that conceptually the validation belongs to the mutation on the input, not the update lifecycle of the component.

Additionally, if you are not using hooks, this will work even if you are using a functional component.

All the above, without the cost of deep comparisons.

Example:

handleChange = (event) => {
  event.preventDefault();
  const { name, value } = event.target;
  let errors = this.state.errors;

  switch (name) {
    case 'fullName': 
      errors.fullName = 
        value.length < 5
          ? 'Full Name must be 5 characters long!'
          : '';
      break;
    case 'email': 
      errors.email = 
        validEmailRegex.test(value)
          ? ''
          : 'Email is not valid!';
      break;
    case 'password': 
      errors.password = 
        value.length < 8
          ? 'Password must be 8 characters long!'
          : '';
      break;
    default:
      break;
  }

  this.setState({errors, [name]: value}, ()=> {
      console.log(errors)
  })
}
Related