1

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'));
skyboyer
  • 22,209
  • 7
  • 57
  • 64
Perplexityy
  • 561
  • 1
  • 9
  • 26
  • Is it actually freezing, or is it just not running because you clear the interval immediately after setting it? I'm legit asking, I don't know React Hooks syntax. But if that's the case you could *conditionally* clear interval instead. – Sydney Y Mar 15 '20 at 11:40

1 Answers1

3

The application freezes because React does not batch your individual state updates. More information about this can be found in this answer

You have two options here.

  1. Use ReactDOM.unstable_batchedUpdates:

This can be done with a single line change, but note that the method is not part of the public API

  useEffect(() => {
    const interval = setInterval(() => {
      // wrap generation function into batched updates
      ReactDOM.unstable_batchedUpdates(() => newGeneration())
    }, 1000);
    return () => clearInterval(interval);
  }, []);
  1. Update all states in one operation.

You could refactor your code to set updated cells only once. This option does not use any unstable methods

  useEffect(() => {
    const interval = setInterval(() => {
       // `newGeneration` function needs to be refactored to remove all `updateCells` calls. It should update the input array and return the result
       const newCells = newGeneration(oldCells);
       // there will be only one call to React on each interval
       updateCells(newCells);
    }, 1000);
    return () => clearInterval(interval);
  }, []);
just-boris
  • 9,468
  • 5
  • 48
  • 84