Trying out variations of code in TicTacToe app. in react tutorial

Note: This post is a details-post for the post: Learning web app development through free online tutorials – Organized Notes and Log, https://raviswdev.blogspot.com/2024/03/learning-web-app-development-through.html .

https://react.dev/learn/tutorial-tic-tac-toe

Too many re-renders. React limits the number of renders to prevent an infinite loop, https://bobbyhadz.com/blog/react-too-many-re-renders-react-limits-the-number . This article gives the scenarios in which this error occurs. In my case, it was due to this scenario: “Calling a function that sets the state in the render method of a component.” It took me quite some time to figure it out. Finally I had this very small test file where the problem was being reproduced. Above page gave me further info. on it. My test file contents are given below:

import { useState } from "react";
export default function MyApp() {
  const [cond, setCond] = useState(true);
  setCond(false); // Dummy statement to demonstrate the problem.
  return <button>Test</button>;
}

The actual scenario where I faced the problem was in initializing a 2D array. A simplified version of that code is given below:

import { useState, createElement } from "react";
function Square({ onClick, value }) {
...
}
function SquaresRow({ onClick, numColumns, rowIndex, gridValues }) {
...
}
function Grid({ onClick, numGridRows, numGridColumns, gridValues }) {
...
}
export default function MyApp() {
  const numGridColumns = 2;
  const numGridRows = 2;
  let [grid, setGrid] = useState([]);
  let tmpGrid = [];
  // https://www.geeksforgeeks.org/how-to-declare-two-dimensional-empty-array-in-javascript/
  for (let i = 0; i < numGridRows; i++) {
    tmpGrid[i] = [];
    for (let j = 0; j < numGridColumns; j++) {
      tmpGrid[i][j] = "0";
    }
  }
  setGrid(tmpGrid); //results in too many re-renders and so React stopping program
  function clickHandler(rowIndex, columnIndex) {
...
  }
  return (
    <>
      <Grid
        onClick={clickHandler}
        numRows={numGridRows}
        numColumns={numGridColumns}
        gridValues={grid}
      />
    </>
  );
}

The need was to initialize the grid 2D array state variable. I could not figure out a way to initialize it in the useState(x) statement. So I did it in the MyApp component main code. I think that seems to be getting rendered again and again (infinite possibility) as setting the state possibly causes the re-render.

One solution to initialize grid 2D array state variable is:

let [grid, setGrid] = useState(
    Array(numGridRows)
      .fill()
      .map(() => Array(numGridColumns).fill("0"))
  ); // https://jalapenojames.medium.com/manipulating-2d-arrays-in-react-d3b69e4b4767

A seemingly stable version of the Grid component app.js is given below:

import { useState, createElement } from "react";
function Square({ onClick, value }) {
  return (
    <button
      onClick={onClick}
      style={{
        display: "inline",
        fontSize: "2rem",
        padding: "20px",
        marginRight: "4px",
        borderWidth: "2px",
        width: "80px",
      }}
    >
      {value}
    </button>
  );
}
function SquaresRow({ onClick, numColumns, rowIndex, rowValues }) {
  console.log(
    `SquaresRow: numColumns: ${numColumns}, rowIndex: ${rowIndex}, rowValues: ${rowValues}`
  );
  let squareRowElements = [];
  for (let i = 0; i < numColumns; i++) {
    squareRowElements[i] = createElement(Square, {
      // Fixes 'Warning: Each child in a list should have a unique "key" prop.'
      key: `Row${rowIndex}Col${i}`,
      onClick: () => onClick(rowIndex, i),
      value: rowValues[i],
    });
  }
  return createElement("div", null, squareRowElements);
}
function Grid({ onClick, numGridRows, numGridColumns, gridValues }) {
  console.log(
    `Grid: numGridRows: ${numGridRows}, numGridColumns: ${numGridColumns}, gridValues: ${gridValues}`
  );
  let rowsOfSquaresElements = [];
  for (let j = 0; j < numGridRows; j++) {
    rowsOfSquaresElements[j] = createElement(SquaresRow, {
      // Fixes 'Warning: Each child in a list should have a unique "key" prop.'
      key: `Row${j}`,
      onClick: onClick,
      numColumns: numGridColumns,
      rowIndex: j,
      rowValues: gridValues[j],
    });
  }
  return createElement("div", null, rowsOfSquaresElements);
}
export default function MyApp() {
  const numGridRows = 6;
  const numGridColumns = 5;
  const [grid, setGrid] = useState(
    Array(numGridRows)
      .fill()
      .map(() => Array(numGridColumns).fill("0"))
  ); // https://jalapenojames.medium.com/manipulating-2d-arrays-in-react-d3b69e4b4767
  function clickHandler(rowIndex, columnIndex) {
    // https://stackoverflow.com/questions/13756482/create-copy-of-multi-dimensional-array-not-reference-javascript
    let newGrid = [];
    for (let i = 0; i < grid.length; i++) newGrid[i] = grid[i].slice();
    newGrid[rowIndex][columnIndex] = "X";
    setGrid(newGrid);
  }
  return (
    <>
      <Grid
        onClick={clickHandler}
        numGridRows={numGridRows}
        numGridColumns={numGridColumns}
        gridValues={grid}
      />
      <button
        onClick={() => alert(`grid (values): ${grid}`)}
        style={{
          padding: "20px",
          marginTop: "20px",
          fontWeight: "bold",
          fontSize: "1rem",
        }}
      >
        Show (alert) state variable grid 2D array values
      </button>
    </>
  );
}

Another solution is by using a useEffect hook with an empty array specified as dependency (no dependencies), which results in the useEffect hook being called only once (or twice if Strict Mode is on and code is run in development mode) at component load time. That approach is covered in more detail later on in this section.

https://react.dev/reference/react/useEffect

Using a useEffect hook with an empty array specified as dependency (no dependencies), as per Dave Gray tutorial (from around 3:05:30), results in the useEffect hook being called only once at component load time. Code for that is given below:

import { useEffect } from 'react';
...
useEffect(() => {
...
}, []);

Tried out the above approach in GridOfSquares program by changing the grid 2D array state variable initialization from complex useState statement given below:

const [grid, setGrid] = useState(Array(numGridRows).fill().map(() => Array(numGridColumns).fill("0")));

to the following code using useEffect:

const [grid, setGrid] = useState([]);
  useEffect(() => {
    let tmpGrid = [];
    // https://www.geeksforgeeks.org/how-to-declare-two-dimensional-empty-array-in-javascript/
    for (let i = 0; i < numGridRows; i++) {
      tmpGrid[i] = [];
      for (let j = 0; j < numGridColumns; j++) {
        tmpGrid[i][j] = "0";
      }
    }
    console.log("In useEffect(): Just before setGrid");
    setGrid(tmpGrid); 
    console.log("In useEffect(): Just after setGrid");
  }, []);

With above code (useEffect), an issue was that useEffect is called only after first rendering of Grid component at which time gridValues is set to empty array (in useState initialization). So the Grid component had to be changed to check for gridValues being empty and in which case, simply returning nothing. Note that the Grid component is rendered again after useEffect function is run, and that would be because the useEffect function changes the grid state variable which is passed as a prop (named gridValues) to Grid component. In this second invocation/rendering of Grid component, the grid state variable (passed as gridValues) is no longer empty and initialized as a 2D array of numGridRows x numGridColumns (two variables in App component). So now the Grid component renders the Grid of Squares as specified in the props passed to it.

I observed that the useEffect() code above, ran twice (as the console log statements in it appeared twice) but the Grid component got rendered only once after useEffect() had run (twice). The following post seems to provide some light on why useEffect() code ran twice and not just once. From React Hooks: useEffect() is called twice even if an empty array is used as an argument, https://stackoverflow.com/questions/60618844/react-hooks-useeffect-is-called-twice-even-if-an-empty-array-is-used-as-an-ar : (React) “StrictMode renders components twice (on dev but not production) in order to detect any problems with your code and warn you about them (which can be quite useful).”

Note that the default app created by create-react-app uses StrictMode. See index.js file.
I commented out the React.StrictMode opening and closing tags in index.js. Then useEffect was invoked only once!

Comments

Archive