Same result on logging the state after render

Viewed 147

I understand that useState is asynchronous. Please read the full question before answering.

I'm trying to modify an element in an array with useState but it doesn't work as expected.

The array and the function to modify it:

const [table, setTable] = useState(['blue', 'blue', 'blue', 'blue', 'blue']);

let currentShapePosition = 2;

function printTable() {
  let newTable = [...table];
  // let newTable = table;
  newTable[currentShapePosition] = 'red';
  setTable(newTable);
  console.log('printTable newTable', newTable); // <-- the result is as expected
  // log => printTable newTable ["blue", "blue", "red", "blue", "blue"]
  console.log('printTable table', table); // <--- The problem is here. I don't get why the array never update
  // log => printTable newTable ["blue", "blue", "blue", "blue", "blue"]
}

Because useState is asynchronous, I understand that the array may not change immediately, but, inside the printTable function the console.log result is the same even after several re-renders.

When instead of: let newTable = [...table]

I do this : let newTable = table

Then the state is updated inside the console.log generated in the function but then there is no re-rendering/component update.

I would like to understand why in the first case newTable = [...table] inside the function the console.log result is the same after several re-renders. And why in the second case newTable = table there is no re-rendering of the component despite setTable(newTable).

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./style.css";

const App = () => {
  let currentShapePosition = 2;

  const [table, setTable] = useState(["blue", "blue", "blue", "blue", "blue"]);

  useEffect(() => {
    printTable();
    window.addEventListener("keydown", handleKeyPress);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    console.log("render", table);
  });

  function printTable() {
    let newTable = [...table];
    // let newTable = table;
    newTable[currentShapePosition] = "red";
    setTable(newTable);
    console.log("printTable newTable", newTable); // <-- the result is as expected
    console.log("printTable table", table); // <--- The problem is here. I don't get why the initial value never change
  }

  function handleKeyPress(event) {
    switch (event.key) {
      case "Left": // IE/Edge specific value
      case "ArrowLeft":
        moveShape(-1);
        break;
      case "Right": // IE/Edge specific value
      case "ArrowRight":
        moveShape(1);
        break;
      default:
        return;
    }
  }

  function moveShape(direction) {
    currentShapePosition += direction;
    printTable();
  }

  return (
    <table className="tetris-table">
      <tbody>
        <tr>
          <td className={table[0]} />
          <td className={table[1]} />
          <td className={table[2]} />
          <td className={table[3]} />
          <td className={table[4]} />
        </tr>
      </tbody>
    </table>
  );
};

const root = document.getElementById("root");

ReactDOM.render(<App />, root);

Edit fervent-field-787ky

3 Answers

The problem is your first useEffect, because you removed the eslint warning, it hidden a potential bug.

useEffect(() => {
  printTable();
  window.addEventListener('keydown', handleKeyPress);
  //   v hidden bug, you should consider the warnings
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

What happened here, is that on component mount the first instance of handleKeyPress assigned to addEventListener (refer to closures), where all array was of value "blue" and it will keep it that way until the unmount.

You should be aware that on every render the component body executed, therefore in your case, there is a new instance of every function.

The same goes for currentPosition that should be a reference.

To fix it, remove the eslint comment and go with the warnings:

useEffect(() => {
  const printTable = () => {
    let newTable = [...table];
    newTable[currentPosition.current] = 'red';
    setTable(newTable);
    console.log('closure', table);
  };

  function handleKeyPress(event) {
    switch (event.key) {
      case 'Left': // IE/Edge specific value
      case 'ArrowLeft':
        moveShape(-1);
        break;
      case 'Right': // IE/Edge specific value
      case 'ArrowRight':
        moveShape(1);
        break;
      default:
        return;
    }
  }

  function moveShape(direction) {
    currentPosition.current += direction;
    printTable();
  }

  window.addEventListener('keydown', handleKeyPress);

  return () => window.removeEventListener('keydown', handleKeyPress);
}, [table]);

Edit Q-58329134-LogTable

Variables created with React methods are special objects with special behaviour ;)

Your function refers to the variable table which is a special React ("state") object. Looks, in such a case, the function always gets the initial state of the variable - otherwise it would show different values at least with the next renders.

If you use a regular table as a variable you will get what is expected. I think this changed version of your code demonstrates that quite well:

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./style.css";

const App = () => {
  let currentShapePosition = 2;

  let tableDemo = ["blue", "blue", "blue", "blue", "blue"];
  function setTableDemo(newTable) {
    tableDemo = newTable.slice();
    setTable(tableDemo); // used to trigger rendering
  }

  // for rendering purposes (to keep the original code structure)
  const [table, setTable] = useState(tableDemo);

  useEffect(() => {
    printTable();
    window.addEventListener("keydown", handleKeyPress);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    console.log("render", table);
  });

  function printTable() {
    let newTable = [...table];
    // let newTable = table;
    newTable[currentShapePosition] = "red";
    setTableDemo(newTable);
    console.log("printTable newTable", newTable); // <-- the result is as expected
    console.log("printTable tableDemo", tableDemo); // <--- The problem is here. I don't get why the initial value never change
  }

  function handleKeyPress(event) {
    switch (event.key) {
      case "Left": // IE/Edge specific value
      case "ArrowLeft":
        moveShape(-1);
        break;
      case "Right": // IE/Edge specific value
      case "ArrowRight":
        moveShape(1);
        break;
      default:
        return;
    }
  }

  function moveShape(direction) {
    currentShapePosition += direction;
    printTable();
  }

  return (
    <table className="tetris-table">
      <tbody>
        <tr>
          <td className={table[0]} />
          <td className={table[1]} />
          <td className={table[2]} />
          <td className={table[3]} />
          <td className={table[4]} />
        </tr>
      </tbody>
    </table>
  );
};

const root = document.getElementById("root");

ReactDOM.render(<App />, root);


Use React "state" objects only to keep "source state values"

Basically it's best to use React "state" objects only to keep "source state values", used to trigger re-rendering. In this specific case the source state value is currentShapePosition. The table as such should not change - only some specific elements of this table. So actually the code, according to React approach, could look like this:

import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";
import "./style.css";

const App = () => {
  const [currentPosition, setPosition] = useState(2);
  const table = useRef(["blue", "blue", "red", "blue", "blue"]);

  function handleKeyPress(event) {
    let newPosition;

    console.log("currentPosition:", currentPosition);
    table.current[currentPosition] = "blue";
    switch (event.key) {
      case "Left": // IE/Edge specific value
      case "ArrowLeft":
        newPosition = currentPosition - 1;
        break;
      case "Right": // IE/Edge specific value
      case "ArrowRight":
        newPosition = currentPosition + 1;
        break;
      default:
    }
    table.current[newPosition] = "red";
    console.log("newPosition:", newPosition);
    console.log("table:", table.current);
    // trigger the new render
    setPosition(newPosition);
  }

  useEffect(() => {
    window.addEventListener("keydown", handleKeyPress);

    return () => window.removeEventListener("keydown", handleKeyPress);
  });

  return (
    <table className="tetris-table">
      <tbody>
        <tr>
          <td className={table.current[0]} />
          <td className={table.current[1]} />
          <td className={table.current[2]} />
          <td className={table.current[3]} />
          <td className={table.current[4]} />
        </tr>
      </tbody>
    </table>
  );
};

const root = document.getElementById("root");

ReactDOM.render(<App />, root);


Of course, the best solution is to render dynamically the <td> elements, with relevant class, without any table. But it's a different story, beyond this question.

Related