react setState callback doesn't have the updated state
Asked Answered
C

4

10

if monthOffset = 12 the condition will evaluate to true and update the yearOffset state to 2017 if yearOffset = 2018. Based on the react docs and other answers I've read, the callback function in this.setState fires after the state has been updated, yet the console.log() is still outputting 2018. I've tried a couple different methods of implementing this code based on answers form other related questions but mine isn't working. I'm not sure why.

handleClick(e) {
  const { monthOffset, yearOffset } = this.state
  this.setState({ monthOffset: monthOffset - 1 })
  if ( monthOffset - 1 === 11 ) { this.setState((prevState) => { 
    return { yearOffset: prevState.yearOffset - 1 } },
    () => {console.log("yearOffset", yearOffset)}
  )}
  console.log("clicked")
}
Clodhopping answered 22/8, 2018 at 1:1 Comment(1)
Have you tried using console.log("yearOffset", yearOffset) instead of an anonymous function inside the setState callback?Sundried
B
6

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")
}
Boland answered 22/8, 2018 at 1:24 Comment(6)
This is a more efficient variation of the code and I got the result I wanted. I think what was wrong was when I called console.log("yearOffset", yearOffset), it was reading the value from the const destructor which I don't think was updated after calling setState. Perhaps for my variation to work I would have to create a new destructor inside the callback.Clodhopping
Just note that you shouldn't use the callback to get information for any reason other than logging it to the console. The callback doesn't always work. You could also use a deconstructor to extract the variables out of the state for a more efficient solution, but from my experience, I've never noticed any latency from making a copy of the entire state itself.Sundried
@Sundried But doesn't the docs say it does always call after the state has been updated? I think the docs are right, I was just reading from the destructor with the wrong value. As I said above I needed to use a new destructor of this.state inside the callback function to get the new value. I tried it and it did work. However this answer is a better way of using the condition and callback so I ended up using it.Clodhopping
The callback does work most of the time, it's just not something that should be completely relied on for any other sort of logic that needs to be performed on the next state. Use it sparingly. You can use it for simple logs to the console, but I wouldn't trust it to return the correct value every time. The docs also say that you should use componentDidUpdate to get the nextState. You can always deconstruct what you want out of the state into a variable (or a constant), alter that variable, and then you have your nextState inside that variable.Sundried
@RyanSam This doesn't answer your question but it might give you some insight: reactjs.org/docs/reconciliation.html#the-diffing-algorithmSundried
Basically, an element only updates if the diffing algorithm catches a change in the state or in the props. If you use the callback, you're not always guaranteed that the state will change. Another tool that might be useful is the npm package "propTypes" but if you're careful about using the correct propTypes, you don't really need this. Using propTypes is sort of like bowling with bumpers. ;)Sundried
S
4

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.

Sundried answered 22/8, 2018 at 1:26 Comment(0)
A
3

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.

Ardenardency answered 22/8, 2018 at 2:0 Comment(0)
C
0

This happens because you are using the yearOffset that you've read on top, from the previous state (before your this.setState has run).
You should instead read the state from the callback directly, like this:

() => console.log("yearOffset", this.state.yearOffset)
Chuddar answered 12/7, 2022 at 9:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.