状态未立即更新时的 ReactJS 表单验证

ReactJS form validation when state is not immediately updated

我正在尝试在注册表单上使用 ReactJS 创建客户端验证。我正在使用 http://validatejs.org/ library for validations along with https://github.com/jhudson8/react-semantic-ui 组件来渲染语义-ui React 组件。这是代码。

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>);
  }
});

我遇到的问题是,当我调用 this.setState 时,状态不会立即更新,所以当我调用 validate(this.state.data, constraints) 时,我正在验证以前的状态,所以用户的UI 体验变得怪异,例如:

如果我的电子邮件字段中有 'example@em' 并输入 'a',它将验证字符串 'example@em' 而不是 'example@ema',因此本质上它总是验证状态在新的击键之前。我必须在这里做一些根本性的错误。我知道组件的状态不会立即更新,只有在渲染完成后才会更新。

我应该在渲染函数中进行验证吗?

--- 解决方案 ---

像 Felix Kling 建议的那样向 setState 添加回调解决了这个问题。这是带有解决方案的更新代码:

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>);
  }
});

--- 更好的解决方案 -----

请参阅下面的 FakeRainBrigand 解决方案。

一个简单的解决方案是在 changeState 方法中检查数据的有效性。

另一个解决方案是传递 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.

当你想从状态中获取数据时,最简单的方法就是在你真正需要它之前。在这种情况下,您只需要在渲染中使用它即可。

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}>
        ...

状态应该很少用作派生数据缓存。如果你确实想在设置状态时派生数据,要非常小心,把它做成一个实例 属性 (e.g. this.errors).

因为 setState 回调实际上会导致额外的渲染周期,您可以改为不可变地更新数据,并将其传递给 this.validate(看看我如何使验证不依赖于 this.state.data 的当前值在上面的代码中?)。

根据您当前的 changeState,它看起来像这样:

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;
 }

在 comments/answers 中,人们说错误应该在状态中,有时确实如此。如果错误处于状态就无法实现渲染,则它们应该处于状态。当您可以通过从状态中派生现有数据来实现时,这意味着将其放入状态将是多余的。

冗余的问题是它增加了追踪错误的可能性。您无法避免将数据保持原样的一个示例是异步验证。没有冗余,因为您不能仅从表单输入中得出它。

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

这就是原因。

为什么不在设置状态之前验证数据?错误 也是状态 ,因此将它们设置为与状态的其余部分相同的方式是合乎逻辑的。

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
    });
},