yoni
yoni

Reputation: 1364

'setState' of useState hook occurs twice by one call

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

Answers (2)

HMR
HMR

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

tanmay
tanmay

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

Related Questions