3

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

Dennis Vash
  • 50,196
  • 9
  • 100
  • 118
Nico
  • 404
  • 6
  • 17
  • Possible duplicate of [Why calling react setState method doesn't mutate the state immediately?](https://stackoverflow.com/questions/30782948/why-calling-react-setstate-method-doesnt-mutate-the-state-immediately) – Emile Bergeron Oct 10 '19 at 18:57
  • Its not duplicate, people please read the question – Dennis Vash Oct 10 '19 at 18:58
  • If it's not a dupe, it's a missing [mcve]... – Emile Bergeron Oct 10 '19 at 19:00
  • It has an example in code-sandbox, I believe it happens because of closures but I admit I'm not 100% with it. – Dennis Vash Oct 10 '19 at 19:02
  • And even your codesandbox seems to work as expected... – Emile Bergeron Oct 10 '19 at 19:02
  • @DennisVash codesandbox doesn't qualify for a valid [mcve]. Anything relevant should be in the question description itself. – Emile Bergeron Oct 10 '19 at 19:03
  • @EmileBergeron I'll edit his question, you should upvoted it, his example **doesn't** work, you can see the `printTable table` always shows the same value – Dennis Vash Oct 10 '19 at 19:04
  • My full code is a tetris. I'm trying to reduce it as much as I can and I admit that I'm not perfectly clear but it's really difficult to define clearly a problem that I do not understand. – Nico Oct 10 '19 at 19:05
  • Also possibly related to [objects in the console being evaluated asynchronously](https://stackoverflow.com/q/23429203/1218980). But either way, I'm seeing the right values when I look in the console. – Emile Bergeron Oct 10 '19 at 19:07
  • 2
    I see these values in the console:printTable newTable ["blue", "blue", "red", "blue", "blue"] printTable table ["blue", "blue", "blue", "blue", "blue"] render ["blue", "blue", "red", "blue", "blue"] and I think that in the second case it should not be always "blue". – Nico Oct 10 '19 at 19:08

3 Answers3

2

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

Dennis Vash
  • 50,196
  • 9
  • 100
  • 118
1

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.

Jacek J
  • 965
  • 1
  • 8
  • 11
0

useState calls are asynchronous, they don't update directly. refer : https://reactjs.org/docs/hooks-state.html

Read more in this answer : useState set method not reflecting change immediately

Dhananjai Pai
  • 5,914
  • 1
  • 10
  • 25
  • I do understand that useState is asynchronous so the array may not change immediately, but, inside the printTable function the console.log result is the same even after several re-renders. Actually, even after the component update, for exemple with a useEffect, inside the print table function the console.log result is the same. – Nico Oct 10 '19 at 18:55
  • Please read the question, I downvoted because it doesn't answer it. – Dennis Vash Oct 10 '19 at 18:59