Home > Enterprise >  Why does my component not auto update after a state change?
Why does my component not auto update after a state change?

Time:09-27

I am following a tutorial on Scrimba for React and currently doing the Boxes challenge part 4.

I pretty much have a function that takes in an ID, and that ID relates to an array of key-value pairs.

const [squares, setSquares] = React.useState(boxes)

function toggle(id) {

        /**
         * Challenge: use setSquares to update the
         * correct square in the array.
         * 
         * Make sure not to directly modify state!
         * 
         * Hint: look back at the lesson on updating arrays
         * in state if you need a reminder on how to do this
         */

        setSquares(oldSquares => {
            let newSquares = oldSquares
            newSquares[id - 1].on = !oldSquares[id - 1].on
            return newSquares
        })
    }

Here is what squares contains:

[{id: 1, on: true}, {id: 2, on: false}, {id: 3, on: true}, {id: 4, on: true}, {id: 5, on: false}, {id: 6, on: false}]

My function above properly inverts the value for the on key and I prove this through print statements, however my component displaying the squares does not update even though the state is updating.

Anyone know why?

CodePudding user response:

You're not creating a new array in state, but mutating the existing one. So the framework doesn't know there's a state update.

This creates a reference to the same array in memory:

let newSquares = oldSquares

This mutates a value in that array:

newSquares[id - 1].on = !oldSquares[id - 1].on

And this returns to state the same reference to the same array:

return newSquares

Don't mutate state. Instead, create a new array with the updated elements:

setSquares(oldSquares => 
  oldSquares.map((s, i) =>
    i === id - 1 ?
      { ...s, on: !s.on } :
      s
  )
)

In this case .map() returns a new array reference, and within the .map() callback we conditionally return a new object with the on property negated if the index equals id - 1 or return the object as-is for all other indices.


For a much more verbose version to perhaps better illustrate the logic, it's effectively the same result as this:

setSquares(oldSquares => {
  let newSquares = oldSquares.map((s, i) => {
    if (i === id - 1) {
      return { ...s, on: !s.on };
    } else {
      return s;
    }
  });
  return newSquares;
})

CodePudding user response:

Your problem is that React doesn't know you've updated your state. You do

let newSquares = oldSquares

and then mutate newSquares (or rather one object inside it), but that variable still holds a reference to the same array that's your current state. React can't "see" that you've changed it.

Instead you need to create a new array reference with the values you need. It would probably work to replace the line quoted above with the following, which creates a shallow copy and therefore a new reference:

let newSquares = [...oldSquares]

But this still makes me nerve because you're updating an object contained within the array, and the above is only taking a shallow copy - so you're still actually mutating your current state. And doing this in React is always liable to introduce subtle bugs.

So better here is to clone more deeply. Since you're only updating a "top level" property of each object in the array, mapping a shallow clone operation over the array would be my choice:

let newSquares = oldSquares.map(obj => ({...obj}));

And actually, while you're doing this, you could get the update you want on the same line, meaning your state update function could be a single line

return oldSquares.map(({on,...rest}, index) => (index === is - 1 ? {on:!on, ...rest} : {on,...rest}));
  • Related