Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ReactJS form validation when state is not immediately updated

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.

like image 511
Simon Polak Avatar asked Jan 24 '15 05:01

Simon Polak


People also ask

Why React state is not updated immediately?

State updates in React are asynchronous; when an update is requested, there is no guarantee that the updates will be made immediately. The updater functions enqueue changes to the component state, but React may delay the changes, updating several components in a single pass.

How do you wait for state update in React?

Use the useEffect hook to wait for state to update in React. You can add the state variables you want to track to the hook's dependencies array and the function you pass to useEffect will run every time the state variables change.

Does React Rerender if state doesn't change?

React components automatically re-render whenever there is a change in their state or props. A simple update of the state, from anywhere in the code, causes all the User Interface (UI) elements to be re-rendered automatically. However, there may be cases where the render() method depends on some other data.

What happens when state is updated in React?

This is a function available to all React components that use state, and allows us to let React know that the component state has changed. This way the component knows it should re-render, because its state has changed and its UI will most likely also change.


2 Answers

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.

like image 85
Brigand Avatar answered Oct 20 '22 09:10

Brigand


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
    });
},
like image 29
Cory Danielson Avatar answered Oct 20 '22 11:10

Cory Danielson