184

Here's my situation:

  • on this.handleFormSubmit() I am executing this.setState()
  • inside this.handleFormSubmit(), I am calling this.findRoutes(); - which depends on the successful completion of this.setState()
  • this.setState(); does not complete before this.findRoutes is called...
  • How do I wait for this.setState() inside of this.handleFormSubmit() to finish before calling this.findRoutes()?

A subpar solution:

  • putting this.findRoutes() in componentDidUpdate()
  • this is not acceptable because there will be more state changes unrelated the findRoutes() function. I don't want to trigger the findRoutes() function when unrelated state is updated.

Please see code snippet below:

handleFormSubmit: function(input){
                // Form Input
                this.setState({
                    originId: input.originId,
                    destinationId: input.destinationId,
                    radius: input.radius,
                    search: input.search
                })
                this.findRoutes();
            },
            handleMapRender: function(map){
                // Intialized Google Map
                directionsDisplay = new google.maps.DirectionsRenderer();
                directionsService = new google.maps.DirectionsService();
                this.setState({map: map});
                placesService = new google.maps.places.PlacesService(map);
                directionsDisplay.setMap(map);
            },
            findRoutes: function(){
                var me = this;
                if (!this.state.originId || !this.state.destinationId) {
                    alert("findRoutes!");
                    return;
                }
                var p1 = new Promise(function(resolve, reject) {
                    directionsService.route({
                        origin: {'placeId': me.state.originId},
                        destination: {'placeId': me.state.destinationId},
                        travelMode: me.state.travelMode
                    }, function(response, status){
                        if (status === google.maps.DirectionsStatus.OK) {
                            // me.response = response;
                            directionsDisplay.setDirections(response);
                            resolve(response);
                        } else {
                            window.alert('Directions config failed due to ' + status);
                        }
                    });
                });
                return p1
            },
            render: function() {
                return (
                    <div className="MapControl">
                        <h1>Search</h1>
                        <MapForm
                            onFormSubmit={this.handleFormSubmit}
                            map={this.state.map}/>
                        <GMap
                            setMapState={this.handleMapRender}
                            originId= {this.state.originId}
                            destinationId= {this.state.destinationId}
                            radius= {this.state.radius}
                            search= {this.state.search}/>
                    </div>
                );
            }
        });
isherwood
  • 58,414
  • 16
  • 114
  • 157
malexanders
  • 3,203
  • 5
  • 26
  • 46

6 Answers6

322

setState() has an optional callback parameter that you can use for this. You only need to change your code slightly, to this:

// Form Input
this.setState(
  {
    originId: input.originId,
    destinationId: input.destinationId,
    radius: input.radius,
    search: input.search
  },
  this.findRoutes         // here is where you put the callback
);

Notice the call to findRoutes is now inside the setState() call, as the second parameter.
Without () because you are passing the function.

wintvelt
  • 13,855
  • 3
  • 38
  • 43
  • 1
    This will work nicely for resetting an AnimatedValue after setState in ReactNative. – SacWebDeveloper Feb 14 '17 at 23:56
  • 3
    A generic version `this.setState({ name: "myname" }, function() { console.log("setState completed", this.state) })` – Sasi Varunan Mar 08 '18 at 10:33
  • It doesn't look like you can pass more than one callback to setState. Is there a non-messy way of chaining callbacks? Lets say i've got 3 methods that all need to run, and all update state. What is the preferred way to handle this? – Sean May 01 '18 at 20:05
  • Without further info, I would think the 1 callback would then be a container, that calls any of your 3 methods (if they need to be fired in sequence). Or the container calls your 3 methods in sequence, and after that does one `setState()` (if you really do not need 4 state changes in a row). Could you elaborate on the specific us case? – wintvelt May 01 '18 at 20:20
  • # Without `()` is really important – M at Dec 01 '21 at 15:26
  • it will work but it's not the right thing to do! setState() will lose its purpose and it will no longer be a pure function. And in case you need the value of the state to be used after some other operation and function calls and not immediately, or your setState() got handled by another function, you will get an error. – ARiyou Jahan Oct 22 '22 at 09:51
  • 1
    @ARiyouJahan I would not go so far to say that "`setState()` will lose its purpose". `setState()` is asynchronous and therefore quite impure. That said, I do agree that in 9 out of 10 cases, there are better ways to solve the issue. If you want something to happen inside the component after the state changes, it is often better to adhere to render cycle logic of react, by putting state somewhere else, or by using lifecycle functions or `useEffect`. – wintvelt Oct 23 '22 at 10:18
51

If someone here landed and having the same situation using hooks, the same behavior can be achived via the below process

const [data, setData] = useState(false);

useEffect(() => {
    doSomething(); // This will be executed when the state changes
}, [data]);

setData(true);

Here useEffect will run after any change in data, and we can execute any dependent task.

Furquan
  • 1,542
  • 16
  • 20
  • 4
    Note, dev might want to skip first render by adding a condition, as the code also will run on first render – cyberfly May 27 '22 at 02:01
  • 1
    we want doSomething to run one time after setData() in our `handler`. NOT every time setData() was called and data got updated in every other function! – ARiyou Jahan Oct 22 '22 at 09:36
  • In these type of cases we can check inside useEffect function , if condition met , then dosomthing(); else skip. can you share more details/code for your scenario ? – Furquan Oct 26 '22 at 20:32
19
this.setState({
    originId: input.originId,
    destinationId: input.destinationId,
    radius: input.radius,
    search: input.search
  },
  function() {
    console.log("setState completed", this.state)
  }
)
isherwood
  • 58,414
  • 16
  • 114
  • 157
Harshit Singhai
  • 1,150
  • 11
  • 7
11

setState takes new state and optional callback function which is called after the state has been updated.

this.setState(
  {newState: 'whatever'},
  () => {/*do something after the state has been updated*/}
)
hackhan
  • 512
  • 3
  • 7
10

According to the docs of setState() the new state might not get reflected in the callback function findRoutes(). Here is the extract from React docs:

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.

So here is what I propose you should do. You should pass the new states input in the callback function findRoutes().

handleFormSubmit: function(input){
    // Form Input
    this.setState({
        originId: input.originId,
        destinationId: input.destinationId,
        radius: input.radius,
        search: input.search
    });
    this.findRoutes(input);    // Pass the input here
}

The findRoutes() function should be defined like this:

findRoutes: function(me = this.state) {    // This will accept the input if passed otherwise use this.state
    if (!me.originId || !me.destinationId) {
        alert("findRoutes!");
        return;
    }
    var p1 = new Promise(function(resolve, reject) {
        directionsService.route({
            origin: {'placeId': me.originId},
            destination: {'placeId': me.destinationId},
            travelMode: me.travelMode
        }, function(response, status){
            if (status === google.maps.DirectionsStatus.OK) {
                // me.response = response;
                directionsDisplay.setDirections(response);
                resolve(response);
            } else {
                window.alert('Directions config failed due to ' + status);
            }
        });
    });
    return p1
}
Community
  • 1
  • 1
Pawan Samdani
  • 1,594
  • 1
  • 10
  • 11
  • this has a serious flaw - passing a literal obj to `setState()` as the new state is not good because it leads to race conditions – tar Jun 10 '19 at 17:34
  • here's another quote from the react docs (which might have been updated since you posted your answer): "...use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied". This says to me that the new state most definitely is reflected in the callback function. – Andy Apr 01 '20 at 17:57
0

Why not one more answer? setState() and the setState()-triggered render() have both completed executing when you call componentDidMount() (the first time render() is executed) and/or componentDidUpdate() (any time after render() is executed). (Links are to ReactJS.org docs.)

Example with componentDidUpdate()

Caller, set reference and set state...

<Cmp ref={(inst) => {this.parent=inst}}>;
this.parent.setState({'data':'hello!'});

Render parent...

componentDidMount() {           // componentDidMount() gets called after first state set
    console.log(this.state.data);   // output: "hello!"
}
componentDidUpdate() {          // componentDidUpdate() gets called after all other states set
    console.log(this.state.data);   // output: "hello!"
}

Example with componentDidMount()

Caller, set reference and set state...

<Cmp ref={(inst) => {this.parent=inst}}>
this.parent.setState({'data':'hello!'});

Render parent...

render() {              // render() gets called anytime setState() is called
    return (
        <ChildComponent
            state={this.state}
        />
    );
}

After parent rerenders child, see state in componentDidUpdate().

componentDidMount() {           // componentDidMount() gets called anytime setState()/render() finish
console.log(this.props.state.data); // output: "hello!"
}
HoldOffHunger
  • 18,769
  • 10
  • 104
  • 133
  • setState() will NOT trigger re-render immediately! consider reading this article: https://reactjs.org/docs/faq-state.html#why-is-setstate-giving-me-the-wrong-value – ARiyou Jahan Oct 22 '22 at 09:41