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}));