3

Returning the (changed) previous state within a setState that one gets from the useState hook doesn't seem to alter state. Run the following straight forward snippet

function App(){
  const [state, setState] = React.useState([{x: 0}])
  function changeCount(){
    setState(prevState => {
      console.log('before', prevState[0])
      const newState = [...prevState]
      newState[0].x += 1 //the shallow copy newState could potentially change state
      console.log('after', prevState[0])//here x gets bigger as expected
      return prevState //instead of newState we return the changed prevState
    })
  }
  //checking state shows that x remained 0
  return <div className='square' onClick={changeCount}>{state[0].x}</div>
}
ReactDOM.render(<App/>, document.getElementById('root'))
.square{
  width: 100px;
  height: 100px;
  background: orange;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id='root'></div>
By clicking the square we trigger setState. Within setState we make a shallow copy newState of the previous state. By changing the copy we change the prevState (maybe unintentionally) as the console confirms. Returning the changed previous state form setState however doesn't change the state as the count remains 0. If we were to return the newState, the behavior would be as expected.

Repeating this, shows that prevState gets bigger, it just doesn't seem to represent the previous state anymore.

Why is that? I made this minimal example on codepen...

Michael
  • 878
  • 5
  • 17
  • thats a good question, trying to find out what is happening. – Tinu Jos K Feb 07 '20 at 05:40
  • @gaditzkhori it's a shallow copy, thats why both prevState and newState is modifying the same x value, and on each renders the prevState actually gets the updated previous value, then why can't we set it onto the state and why is it not displayed in the div? – Tinu Jos K Feb 07 '20 at 05:55
  • 1
    `setState` is comparing the previous state object to the object you're returning. If both these reference the same object, no render takes place. Despite the fact that prevState has been updated, it's still the same object. – FuriousD Feb 07 '20 at 05:58
  • 2
    newState is a shallow copy but you changed `x` which is the same reference.React doesn't know that it is changed deeply and therefore doesn't rerender.so basicly it's pure js not some React magic.thats the whole idea of shallow rendering. – gadi tzkhori Feb 07 '20 at 06:02
  • @Furious and gadi, if we return newState instead of prevState it works as expected, but here we have to note that newState is also a shallow copy referring to the same object. – Tinu Jos K Feb 07 '20 at 06:04
  • It refers to the same `x` pointer only – gadi tzkhori Feb 07 '20 at 06:06
  • `newState !== prevState` it is not the same object – gadi tzkhori Feb 07 '20 at 06:13
  • 1
    you are right! prevState gets updated as it should be, but no rerender takes place because of the deep change of the object. forcing a rerender by changing some extra dummy state variable shows that the count goes up correctly – Michael Feb 07 '20 at 06:53
  • The selected answer is a good overview on [value vs reference](https://codeburst.io/explaining-value-vs-reference-in-javascript-647a975e12a0) but I think @gaditzkhori comment about [shallow copies](https://stackoverflow.com/questions/43040721/how-to-update-nested-state-properties-in-react#51136076) actually answers the question. – sallf Feb 07 '20 at 07:43

1 Answers1

5

Consider that object assignment is just a reference assignment, never a copy

obj1 = {x:42, y:99};
obj2 = obj1;   // obj1 and obj2 both reference the same object
obj1.x += 1;   
obj2.y += 1;
console.log(obj1.x, obj1.y);
console.log(obj2.x, obj2.y);  // prints the same thing since obj1 and obj2 are the same object

In the above example, obj1 is initialized to point to a new object with properties x and y. When obj2=obj1 is made, this is not a copy of obj1 into obj2, but rather obj1 and obj2 now reference the same object.

Hence, when the console.log statements print, they print the same thing because they are both printing property values from the same object.

Similarly, the additional reference to the original object is being made when the shallow copy from prevState to newState occurrs.

obj = {x:42, y:99};
prevState[0] = obj;     // prevState[0] is a reference to obj.  prevState[0] and obj point to the same exact thing

newState = [...prevState];  // shallow copy, but newState[0] is an object reference.  newState[0] and prevState[0] both point to "obj"

newState[0].x += 1;         // is actually updating the original object assigned to prevState[0]
selbie
  • 100,020
  • 15
  • 103
  • 173
  • thanks for the nice explanation! i was puzzled by the fact that although prevState gets changed as one could expect, one doesn’t observe the new state. the reason was, there was just no rerender because the change of state was a deep change, while the object itself was still the same object. no changed state object, no rerender... – Michael Feb 07 '20 at 06:57
  • @Michael even though it's the same object returned (prevState), as it's value gets changed (value of x), why doesn't React consider a re-render? – Tinu Jos K Feb 07 '20 at 07:11
  • 1
    @Tick20 because state comparison is just shallow comparison (i.e same object reference doesn't cause re-render). Likewise related is how `setState` in functional component only does shallow merge. – Joseph D. Feb 07 '20 at 07:42