Home > front end >  Using state variable values after promise resolution in react
Using state variable values after promise resolution in react

Time:09-23

I have used the following pattern in previous ReactJS projects - A LOT!

componentDidMount() {
    var promises = [];
    var promise1 = this.AsyncFunc1().then(res => {
        this.setState({some_state1: res.data.results});
    }).catch(error => {
        //deal with error
    });
    var promise2 = this.AsyncFunc2().then(res => {
        this.setState({some_state2: res.data.results});
    }).catch(error => {
        //deal with error
    });
    promises.push(promise1);
    promises.push(promise2);
    Promise.all(promises).then(() => {
        // Use the state variables
    }).catch(error => {
        // deal with error
    });
}

I understand that state is set asynchronously and not available right away - I avoided accessing the state immediately following the setting of that state in the then statements of the async functions - but I have consistently accessed the state variable values in the promise resolution section in previous projects - but in this new project I'm working on it is failing off and on because the state variable values aren't consistently available. I've resorted to setting temp variables to the res.data.results values and using those in the Promise.all section. My previous projects have been in production for 2 years without issues (that I'm aware of) - do I need to go back and rework that code? Did I make a bad assumption that the state variable values would be available in the Promise.all section? I appreciate the help!

CodePudding user response:

Yes, you made an incorrect assumption; your promise might be called before the state is available. From setState docs, emphasis mine:

setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state. This is the primary method you use to update the user interface in response to event handlers and server responses.

...

setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.

There's also a FAQ entry about your kind of case, emphasis again mine:

Why is setState giving me the wrong value?

In React, both this.props and this.state represent the rendered values, i.e. what’s currently on the screen.

Calls to setState are asynchronous - don’t rely on this.state to reflect the new value immediately after calling setState. Pass an updater function instead of an object if you need to compute values based on the current state (see below for details).

As in the second quote, the state variables won't be updated until after the render happens, and even though you're not immediately reading state after calling setState, it's still likely that your Promise.all will fire before the this.state is updated—and React absolutely reserves the right to do so. Auditing your previous use of this pattern is probably a good idea.


Some options for you if you really want the render to happen first:

  • Use componentDidUpdate and check that both state variables are set, which the FAQ recommends.
  • Use the second setState parameter, updater, which will call you back after the setState takes effect. This is not recommended compared to componentDidUpdate.
  • With hooks, use useEffect. You can pass your some_state1 and some_state2 in the second argument to useEffect to only take effect when the values change.
  • Within your AsyncFunc then handlers, wrap your code in flushSync. The docs heavily recommend against this option: flushSync is not as specific as the other options above, but for a stopgap measure on old code it might be effective for you.

If you don't need the UI to update before your Promise.all() handler runs, you can save the resolved values to local variables, or you can make sure to return the values from your AsyncFunc then handlers. If your // deal with error handlers provide fallback values, you can return those from your catch handlers there too; conversely, if you don't want your Promise.all() handler to run if either of those calls fail, you should ensure that they re-throw the exceptions from within the catch block so the promises you save stay rejected.

(Of course, you could also move both of your setState calls to the Promise.all handler so you don't even try to call setState before both values are there, but I presume you want the partial UI update that comes from each individual AsyncFunc.)

  • Related