Been learning some react and typescript and wanted to get some feedback on this small project I did. Feel free to be as nit-picky as you like.
import './App.css'
import Button from './components/button';
import Grid from './components/grid'
import { useEffect, useRef, useState } from 'react';
// This is me trying to make conway's game of line in typescript + React. don't ask me why
function App() {
  // set size of grid
  const cols = 9;
  const rows = 10;
  // Create the 2d array to manage cell state
  const createGrid = (rows: number, cols: number) => { 
    const cell_states: boolean[][] = []; 
    for (let i = 0; i < rows; i++) {
      cell_states[i] = [];
      for (let j = 0; j < cols; j++) {
        cell_states[i][j] = false
      }
    }
    return cell_states;
  }
  const [cell_states, setCellStates] = useState<boolean[][]>(createGrid(rows, cols));
  // indexes of current cell
  const [rowIdx, setRowIdx] = useState(0);
  const [colIdx, setColIdx] = useState(0);
  // loop is running
  const [isRunning, setIsRunning] = useState(false);
  // flipped ref
  const flipped = useRef(false);
  const sign = useRef<number>(1);
  // 1 if forwards; -1 if backwards
  const backwards = useRef<number>(1);
  const end_tup: [row: number, col: number] = cols % 2 === 0 ? [0, cols - 1]: [rows - 1, cols - 1];
  const end = useRef<[rows: number, cols: number]>(end_tup);  
  // useEffect triggers during startup and when states in the dependency array changes
  useEffect(() => {
    // if it's not running i.e. button hasn't been pressed yet.
    if(!isRunning) return;
    // don't quite understand myself but we use timeouts to set a delay between to emulate animation
    const timeoutId: number = setTimeout(() => {
      // tick when flipped is on will just remove the row changes from the previous tick so that in the next tick it starts at end;
      if (flipped.current) {
        flipped.current = false;
        setRowIdx(prev => prev += (1 * sign.current));
        return;
      }
      // toggle cell function; dead -> alive, alive -> dead
      const toggle = (row: number, col: number) => {
        setCellStates((prev) =>  {
          const newGrid = [...prev];
          newGrid[row] = [...prev[row]];
          newGrid[row][col] = !newGrid[row][col];
          return newGrid;
        })
      }
      toggle(rowIdx, colIdx); 
      
      // row and column update
      if ((rowIdx < rows - 1  && sign.current === 1)|| rowIdx > 0 && sign.current === -1) 
      {
        setRowIdx(prev => prev += (1 * sign.current)); // every non row edge cell
      } 
      else if (end.current[0] === rowIdx && end.current[1] === colIdx) // when row,col reaches the end square
      {
        // switch the signs, and flow to backwards
        backwards.current = backwards.current * -1;
        sign.current = sign.current * -1;
        // set end as the opposite diagonal coordinate
        const end_row: number = cols % 2 === 0 ? 0: (end.current[0] * -1) + rows - 1;
        const end_col: number = (end.current[1] * -1)  + cols - 1;
        end.current = [end_row, end_col];
        // set flipped to true for the end edge case
        flipped.current = true;
        // this will set the row index out of bounds in the next tick but needed to retrigger rerender
        setRowIdx(prev => prev += (-1 * sign.current))
      } else 
      { 
        // moving to the next column
        setColIdx(prev => prev += (1 * backwards.current));
        sign.current = sign.current * -1;
      }
      
    }, 50);
    return () => clearTimeout(timeoutId);
  }, [colIdx, isRunning, rowIdx]);
  const handleClick = () => {
    setCellStates(createGrid(rows, cols));
    setRowIdx(0);
    setColIdx(0);
    flipped.current = false;
    sign.current = 1;
    backwards.current = 1;
    setIsRunning(true);
  };
  return (
    <>
      <Grid col_size={cols} rows={rows} states={cell_states} ></Grid> 
      <Button OnClick={handleClick}></Button>
    </>
  )
}
export default App
