I am trying to implement Conway's Game of Life in React, but it is freezing whenever a new generation is called. I assume this is because there is too much overhead caused by constantly re-rendering the DOM, but I don't know how to resolve this, nor can I think of an alternative to simply posting my entire code, so I apologise in advance for the verbosity.
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import styled from "styled-components"
interface TileProps {
    bool: boolean
}
const Tile: React.FC<TileProps> = ({bool}) => {
    const colour = bool  == true ? "#00FF7F" : "#D3D3D3" 
    return (
        <div style = {{backgroundColor: colour}}/>
    )
}
interface GridProps {
    cells: boolean[][]
}
const StyledGrid = styled.div`
    display: grid;
    grid-template-columns: repeat(100, 1%);
    height: 60vh;
    width: 60vw;
    margin: auto;
    position: relative;
    background-color: #E182A8;
`
const Grid: React.FC<GridProps> = ({cells}) => {
    return (
        <StyledGrid>
            {cells.map(row => row.map(el => <Tile bool = {el}/>))}
        </StyledGrid>
    )
}
const randomBoolean = (): boolean => {
  const states = [true, false];
  return states[Math.floor(Math.random() * states.length)]
}
const constructCells = (rows: number, columns: number): boolean[][] => {
  return constructEmptyMatrix(rows, columns).map(row => row.map(e => randomBoolean()))
}
const constructEmptyMatrix = (rows: number, columns: number): number[][] => {
  return [...Array(rows)].fill(0).map(() => [...Array(columns)].fill(0));
}
const App: React.FC = () => {
  const columns = 100;
  const rows = 100;
  const [cells, updateCells] = useState<boolean[][]>(constructCells(rows, columns));
  useEffect(() => {
    const interval = setInterval(() => {
      newGeneration();
    }, 1000);
    return () => clearInterval(interval);
  }, []);
  const isRowInGrid = (i: number): boolean => 0 <= i && i <= rows - 1
  const isColInGrid = (j : number): boolean => 0 <= j && j <= columns -1
  const isCellInGrid = (i: number, j: number): boolean => {
    return isRowInGrid(i) && isColInGrid(j)
  }
  const numberOfLiveCellNeighbours = (i: number, j: number): number => {
    const neighbours = [
      [i - 1, j], [i, j + 1], [i - 1, j + 1], [i - 1, j + 1],
      [i + 1, j], [i, j - 1], [i + 1, j - 1], [i + 1, j + 1]
    ]
    const neighboursInGrid = neighbours.filter(neighbour => isCellInGrid(neighbour[0], neighbour[1]))
    const liveNeighbours = neighboursInGrid.filter(x => {
      const i = x[0]
      const j = x[1]
      return cells[i][j] == true
    })
    return liveNeighbours.length;
  }
  const updateCellAtIndex = (i: number, j: number, bool: boolean) => {
    updateCells(oldCells => {
      oldCells = [...oldCells]
      oldCells[i][j] = bool;
      return oldCells;
    })
  }
  const newGeneration = (): void => {
    cells.map((row, i) => row.map((_, j) => {
      const neighbours = numberOfLiveCellNeighbours(i, j);
      if (cells[i][j] == true){
        if (neighbours < 2){
          updateCellAtIndex(i, j, false);
        } else if (neighbours <= 3){
          updateCellAtIndex(i, j, true);
        }
        else {
          updateCellAtIndex(i, j, false);
        }
      } else {
        if (neighbours === 3){
          updateCellAtIndex(i, j, true);
        }
      }
    }))
  }
  return (
    <div>
      <Grid cells = {cells}/>
    </div>
  )
}
ReactDOM.render(<App />, document.getElementById('root'));
 
     
    