react setState callback doesn't have the updated state

Perhaps you could simplify your logic in the following way, to avoid multiple calls to setState which may be causing unexpected results:

handleClick(e) {

  const { monthOffset, yearOffset } = this.state

  // call setState once
  this.setState({ 

    // Always decrement month offset
    monthOffset : monthOffset - 1, 

    // Only decrement year offset if current month offset === 12
    yearOffset : (monthOffset === 12) ? yearOffset - 1 : yearOffset

  }, () => {

    console.log("state updated to", this.state)
  })

  console.log("clicked")
}

There are two prevalent patterns to calling setState in React: object setState, and "functional setState". Functional setState is generally used when the current state (or "previous state", or whatever you want to call the old state) is invoked in the setState call. This is done because setState is asynchronous, and as a result subsequent setStates can sometimes run before React has managed to complete the first setState cycle.

You have used existing state in your setState call, so it would be an appropriate place to use functional setState. Replace

this.setState({ monthOffset: monthOffset - 1 })

with

this.setState(monthOffset => {return {monthOffset: monthOffset - 1}})

If you are like me when I first saw this, you might be thinking, "Huh? How is that any different than what I have?" The difference is that when setState is passed a function instead of an object, it queues the update instead of going through its usual resolution process, which ensures things get done in order.

Or you might not be thinking this; you have actually used functional setState in your second setState call. Using it in your first one too will make sure things are queued correctly.


The documentation says that the callback always works, but I know from experience that it doesn't always return what you're expecting. I think it has something to do with using mutable objects inside the state itself.

Docs: https://reactjs.org/docs/react-component.html#setstate

You can't completely rely on the callback. Instead, what you can do is create

var stateObject = this.state

Make any necessary changes to the object:

stateObject.monthOffset -= 1

and then set the state like this:

this.setState(stateObject);

That way you have a copy of the nextState inside stateObject

To clarify: You want to do all of the evaluation before you set the state, so do:

monthOffset -= 1

then if (monthOffset === 12) yearOffset -=1;

then var stateObj = {monthOffset: monthOffset, yearOffset: yearOffset}

then this.setState(stateObj);


From the documentation: "The second parameter to setState() is an optional callback function that will be executed once setState is completed and the component is re-rendered. Generally we recommend using componentDidUpdate() for such logic instead."

So basically if you want to get the next state, you should either have a copy of it inside your function that calls setState() or you should get the nextState from componentDidUpdate


Afterthoughts: Anything that you pass into setState() as a parameter is passed by reference (not by value). So, if you have an object SearchFilters: {} within your state, and inside your call to setState(), you have

setState({SearchFilters: DEFAULT_SEARCH_FILTERS}); // do not do this

You may have set SearchFilters to DEFAULT_SEARCH_FILTERS in an effort to clear out a form called "Search Filters", but instead you will have effectively set DEFAULT_SEARCH_FILTERS (a constant) to SearchFilters, clearing out your DEFAULT_SEARCH_FILTERS.

Expected behavior? You tell me.