Reputation: 1364
There is a board with squares their value relies on an array, it is handled with useState
hook. Every click should raise the value by one, but unfortunately, it raises it by two (except the first click).
My questions are:
(1) Why is it happen, (2) how to avoid it, and, in general, (3) is there a better way to handle such an array with hooks.
let emptyBoard = Array.from({ length: parseInt(props.rows, 10) }, () =>
new Array(parseInt(props.columns, 10)).fill(0)
);
const [squaresValues, setSquaresValue] = useState(emptyBoard);
function onClick(id) {
const [rowIndex, cellIndex] = id;
console.log("the " + id + " square was clicked");
setSquaresValue(prevValues => {
let newBoard = [...prevValues];
console.log("before: " + newBoard[rowIndex][cellIndex]);
newBoard[rowIndex][cellIndex] = newBoard[rowIndex][cellIndex] + 1;
console.log("after: " + newBoard[rowIndex][cellIndex]);
return newBoard;
}
);
}
The log:
the 3,0 square was clicked
before: 0
after: 1
the 3,0 square was clicked
before: 1
after: 2
before: 2
after: 3
As can be seen, from the second click the value is raised twice by every click.
Upvotes: 4
Views: 6036
Reputation: 39320
You were still mutating state, if you have pure components then they won't re render when mutating. Doing the full state copy with JSON.parse is a bad idea if you have pure components because everything will be re rendered.
let newBoard = [...prevValues];
newBoard[rowIndex] = [...newBoard[rowIndex]];
newBoard[rowIndex][cellIndex] =
newBoard[rowIndex][cellIndex] + 1;
Upvotes: 6
Reputation: 7911
As mentioned by Udaya Prakash in the comment above, it's being called twice to make sure your setState is independent and idempotent. So, if I understand correctly, it being called twice is not a bug, but your values being changed the second time is.
Here's Dan Abramov's comment from the same GitHub issue:
It is expected that setState updaters will run twice in strict mode in development. This helps ensure the code doesn't rely on them running a single time (which wouldn't be the case if an async render was aborted and alter restarted). If your setState updaters are pure functions (as they should be) then this shouldn't affect the logic of your application.
We can fix it by deep-copying your prevValues
instead of shallow-copying with spread operator. As you might already know, there are multiple ways to deep copy your object, we could go with JSON.parse(JSON.stringify(...)
for now, which you can replace with your favorite kind from here
setSquaresValue(prevValues => {
let newBoard = JSON.parse(JSON.stringify(prevValues)); // <<< this
console.log("before: " + newBoard[rowIndex][cellIndex]);
newBoard[rowIndex][cellIndex] = newBoard[rowIndex][cellIndex] + 1;
console.log("after: " + newBoard[rowIndex][cellIndex]);
return newBoard;
});
I've kind of simulated it in codesandbox here if you want to play around.
Upvotes: 5