ReactJS form validation when state is not immediately updated
Asked Answered
A

3

10

I am trying to create client side validation with ReactJS on my registration form. I am using http://validatejs.org/ library for validations along with https://github.com/jhudson8/react-semantic-ui components for rendering semantic-ui React components. Here is the code.

var constraints = {
  email: {
    presence: true, 
    email:true
  }, 
  password: {
    presence: true,
    length: { minimum: 5 }
  }
}

var RegistrationForm = React.createClass({

  getInitialState: function() {
    return { 
      data: {},
      errors: {}
    };
  },

  changeState: function () {
    this.setState({
      data: {
        email: this.refs.email.getDOMNode().value,
        password: this.refs.password.getDOMNode().value,
        password_confirmation:  this.refs.password_confirmation.getDOMNode().value
      }
    });
    console.log("State after update");
    console.log(this.state.data);
  },

  handleChange: function(e) {
    this.changeState();
    var validation_errors = validate(this.state.data, constraints);

    if(validation_errors){
      console.log(validation_errors);
      this.setState({errors: validation_errors});
    }
    else
      this.setState({errors: {}});
  },

  handleSubmit: function(e) {
    e.preventDefault();
    //code left out..
  },

  render: function() {
    var Control = rsui.form.Control;
    var Form = rsui.form.Form;
    var Text = rsui.input.Text;
    var Button = rsui.form.Button;
    return (
      <Form onSubmit={this.handleSubmit} onChange={this.handleChange}>
        <h4 className="ui dividing header">New User Registration</h4>
        <Control label="Email" error={this.state.errors.email}>
          <Text name="email" type="email" ref="email"  key="email" value={this.state.data.email}></Text>
        </Control>
        <Control label="Password" error={this.state.errors.password}>
          <Text name="password" type="password" ref="password" key="password" value={this.state.data.password}></Text>
        </Control>
        <Control label="Password Confirmation">
          <Text name="password_confirmation" type="password" ref="password_confirmation" key="password_confirmation" value={this.state.data.password_confirmation}></Text>
        </Control>
        <Button> Register </Button>
      </Form>);
  }
});

The problem I am having is that when I call this.setState, the state is not immediately updated, so when I call validate(this.state.data, constraints) I am validating previous state, so user's UI experience gets weird, for example:

If I have 'example@em' in my email field and I enter 'a', it will validate string 'example@em' not 'example@ema', so in essence it always validates the state before the new key stroke. I must be doing something fundamentally wrong here. I know state of the component is not updated right away, only after render is done.

Should I be doing validations in render function ?

--- SOLUTION ---

Adding a callback to setState like Felix Kling suggested solved it. Here is the updated code with solution:

var RegistrationForm = React.createClass({

  getInitialState: function() {
    return { 
      data: {},
      errors: {}
    };
  },

  changeState: function () {
    this.setState({
      data: {
        email: this.refs.email.getDOMNode().value,
        password: this.refs.password.getDOMNode().value,
        password_confirmation: this.refs.password_confirmation.getDOMNode().value
      }
    },this.validate);
  },

  validate: function () {
    console.log(this.state.data);
    var validation_errors = validate(this.state.data, constraints);

    if(validation_errors){
      console.log(validation_errors);
      this.setState({errors: validation_errors});
    }
    else
      this.setState({errors: {}});
  },

  handleChange: function(e) {
    console.log('handle change fired');
    this.changeState();
  },

  handleSubmit: function(e) {
    e.preventDefault();
    console.log(this.state);
  },

  render: function() {
    var Control = rsui.form.Control;
    var Form = rsui.form.Form;
    var Text = rsui.input.Text;
    var Button = rsui.form.Button;
    return (
      <Form onSubmit={this.handleSubmit} onChange={this.handleChange}>
        <h4 className="ui dividing header">New Rowing Club Registration</h4>
        <Control label="Email" error={this.state.errors.email}>
          <Text name="email" type="email" ref="email"  key="email" value={this.state.data.email}></Text>
        </Control>
        <Control label="Password" error={this.state.errors.password}>
          <Text name="password" type="password" ref="password" key="password" value={this.state.data.password}></Text>
        </Control>
        <Control label="Password Confirmation">
          <Text name="password_confirmation" type="password" ref="password_confirmation" key="password_confirmation" value={this.state.data.password_confirmation}></Text>
        </Control>
        <Button> Register </Button>
      </Form>);
  }
});

--- BETTER SOLUTION -----

See FakeRainBrigand's solution below.

Ailment answered 24/1, 2015 at 5:37 Comment(0)
D
9

When you want to derive data from state, the simplest way to do it is right before you actually need it. In this case, you just need it in render.

validate: function (data) {
  var validation_errors = validate(data, constraints);

  if(validation_errors){
    return validation_errors;
  }

  return {};
},

render: function() {
    var errors = this.validate(this.state.data);
    ...
      <Control label="Email" error={errors.email}>
        ...

State should very rarely be used as a derived data cache. If you do want to derive data when setting state, be very careful, and just make it an instance property (e.g. this.errors).

Because the setState callback actually causes an additional render cycle, you can immutably update data instead, and pass it to this.validate (see how I made validate not depend on the current value of this.state.data in the above code?).

Based on your current changeState, it'd look like this:

changeState: function () {
  var update = React.addons.update;
  var getValue = function(ref){ return this.refs[ref].getDOMNode().value }.bind(this);

  var data = update(this.state.data, {
      email: {$set: getValue('email')},
      password: {$set: getValue('password')},
      password_confirmation: {$set: getValue('password_confirmation')}
   });

   this.errors = this.validate(data);
   this.setState({data: data});
 },

 // we'll implement this because now it's essentially free 
 shouldComponentUpdate: function(nextProps, nextState){
   return this.state.data !== nextState.data;
 }

In the comments/answers people are saying that errors should be in state, and that's sometimes true. When you can't implement render without the errors being in state, they should be in state. When you can implement by deriving existing data from state, that means that putting it in state would be redundant.

The problem with redundancy is it increases the likelihood of very difficult to track down bugs. An example of where you can't avoid keeping the data as state is with async validation. There's no redundancy, because you can't derive that from just the form inputs.

I made a mistake of not updating the state of errors too. – blushrt

This is exactly why.

Deckert answered 24/1, 2015 at 16:43 Comment(4)
Thank you! This solution has a lot less rendering. I was not aware of React addons. One thing I had to do is add .bind(this) to getValue function, so this.refs call would have the correct this context.Ailment
For anyone that is using react-rails gem, be sure to add config.react.addons = true to application.rb so you can acces React.addonsAilment
But I don't agree that form errors are derived data cache here, they should be part of the state too.Ailment
@CoryDanielson, see this page in the docsDeckert
I
1

Why wouldn't you just validate the data before you set the state? The errors are also state, so it would be logical to set them in the same fashion as the rest of the state.

changeState: function () {
    var data = {
            email: this.refs.email.getDOMNode().value,
            password: this.refs.password.getDOMNode().value,
            password_confirmation: this.refs.password_confirmation.getDOMNode().value
        },
        errors = validate(data, constraints) || {};

    this.setState({
        data: data,
        errors: errors
    });
},
Interfere answered 25/1, 2015 at 7:8 Comment(1)
I also think the errors should be part of the form state.Ailment
T
0

A simple solution would be to check the validity of the data in the changeState method instead.

The other solution would be to pass a callback to setState:

In addition, you can supply an optional callback function that is executed once setState is completed and the component is re-rendered.

[...]

setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value.

There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains.

Telefilm answered 24/1, 2015 at 5:44 Comment(3)
Checking validity of data in changeState didn't work, but adding a callback to setState worked. Thanks! I know this is probably not an optimal solution, I see that validation in react can be quite complex, I found this great resource christianalfoni.github.io/javascript/2014/10/22/…, this would probably be a better solution.Ailment
@blushrt why didn't checking validity during changeState work?Interfere
@CoryDanielson, I made a mistake of not updating the state of errors too. So it should work now.Ailment

© 2022 - 2024 — McMap. All rights reserved.