Multiple setState() calls in a React method: How to make it work “synchronously”

萝らか妹 提交于 2020-01-06 15:18:58

问题


So I had a problem in my React application, I ran into a specific case where I needed to have multiple setState() calls in one method, and then have code run AFTER the states were set. The code below is a Dialog box used to add an account on the website.

import React from 'react';
import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import TextField from 'material-ui/TextField';

/**
 * A modal dialog can only be closed by selecting one of the actions.
 */
export default class NewAcctDia extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      open: false,
      userError: null,
      passError: null,
      passConfirmError: null,
    }

    this.handleOpen = this.handleOpen.bind(this);
    this.handleClose = this.handleClose.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleOpen() {
    this.setState({open: true});
  }

  handleClose() {
    this.setState({open: false});
  }

  handleSubmit() {
    if(!this.refs.user.getValue())
      this.setState({userError: "This field is required"});
    else
      this.setState({userError: null});

    if(!this.refs.pass.getValue())
      this.setState({passError: "This field is required"});
    else
      this.setState({passError: null});

    if(this.refs.pass.getValue() == this.refs.passConfirm.getValue()) {
      this.setState({passError: null});
    } else {
      this.setState({passConfirmError: "Passwords do not match"});
    }

    if(!this.state.userError && !this.state.passError && !this.state.passConfirmError)
      alert('worked');
  }

  render() {
    const actions = [
      <FlatButton
        label="Cancel"
        primary={true}
        onTouchTap={this.handleClose}
      />,
      <FlatButton
        label="Submit"
        primary={true}
        disabled={false}
        onTouchTap={this.handleSubmit}
      />,
    ];

    return (
        <Dialog
          title="Create an Account"
          actions={actions}
          modal={true}
          open={this.state.open}
          contentStyle={{width: 350}}
        >
          <TextField
            ref='user'
            floatingLabelText="Username"
            errorText={this.state.userError}
          /><br />
          <TextField
            ref='pass'
            floatingLabelText="Password"
            type="password"
            errorText={this.state.passError}
          /><br />
          <TextField
            ref='passConfirm'
            floatingLabelText="Confirm Password"
            type="password"
            errorText={this.state.passConfirmError}
          /><br />
        </Dialog>
    );
  }
}

The problem is in the handleSubmit() method, I needed to check to see that the user had entered something into the username and password fields, and that the passwords and confirm password fields matched. If they didn't, I would add error text to the fields that needed alteration via state. I then tried to look at the state to see if there were any errors.

Unfortunately, as I quickly figured out, the setState() function is asynchronous, meaning the state would not get changed before my final check. I googled and searched all over for a way to wait for state to change before executing code but came up empty. I've solved this problem now and figured I would put it up on Stack so others might benefit from the method I came up with. I would also like to know any pros and cons of what I am doing, or any suggestions that might work better.

When I googled around I came across a method to send a callback to setState() as shown: setState(data, callback). I did not think this would work for me at first, as I had multiple setState() calls. However, I realized I could convert the handleSubmit() method to a single setState() call with ternaries. Like so:

handleSubmit() {
    this.setState({
      userError: (
        this.refs.user.getValue() ? null : "This field is required"
      ),
      passError: (
        this.refs.pass.getValue() ? null : "This field is required"
      ),
      passConfirmError: (
        (this.refs.pass.getValue() == this.refs.passConfirm.getValue()) ? 
          null : "Passwords do not match"
      )
    }, () => {
      if(!this.state.userError && !this.state.passError && !this.state.passConfirmError)
        alert('worked');
    })
}

The anonymous callback function will be executed after the state is changed, allowing my check to work.

The only problems I foresee with this method are nested ternaries, as that may become extremely messy if they are required. Is there any adverse effects this might have on my program? Or better ways I could have solved this?

I hope I've helped some people with my solution. :D


回答1:


React documentation encourages the use of componentDidUpdate instead of the callback parameter of setState.

Yet, if you think the code seems a bit messy, try out local consts and make a single call to setState:

handleSubmit() {
  const {user, pass, passConfirm} = this.refs;
  const userError = user.getValue() ? null : "This field is required";
  const passError = pass.getValue() ? null : "This field is required";
  const passConfirmError = !passError && pass.getValue() === passConfirm.getValue()
    ? null
    : "Passwords do not match";

  this.setState({userError, passError, passConfirmError});

  if(!userError && !emptyPassError && !passConfirmError)
    alert('worked');
}

Finally, documentation also recommends the use of callback refs over strings refs:

Using the ref callback just to set a property on the class is a common pattern for accessing DOM elements. If you are currently using this.refs.myRefName to access refs, we recommend using this pattern instead.



来源:https://stackoverflow.com/questions/41007302/multiple-setstate-calls-in-a-react-method-how-to-make-it-work-synchronously

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!