Build the Game of Life with React
Tags: react, konvajs, gameoflife
Published: 2021-08-29 13:53:33

---
title: Build the Game of Life with React
published: true
description: Build the Game of Life with React
series: Build the Game of Life
tags: react, konvajs, gameoflife
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1ul67bhpc5iyokiaulb4.jpg
 
---
### Motivation
I've recently read a post about an interviewer who likes to ask of their candidates to implement [Conway's Game of Life] (https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life). Naturally I started thinking how I would do it. Since I'm intrigued by [Blazor](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor) (because C#) and I use React at work (because it's better), here we are about to see how you can build the Game of Life, first with React and in a later post with Blazor.

I plan to group these posts into a series, so that each stays digestible and you can read the one that interests you.

Let me know in the comments if you're interested in seeing implementations in Xamarin.Forms/MAUI, WPF or Flutter.

________________

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y96bkbaz620taydfy6wx.gif)

Here's the code: https://github.com/mariusmuntean/GameOfLife
________________

## Create the React project

Create a new React project with `npx`, give it a name and choose Typescript
```bash 
npx create-react-app gol.react --template typescript
```

## Business Logic
In the `src` directory, create a new one for the new types that you're going to add. I named mine `models`. Add a file for an enum that represents the state of a single cell
```typescript
export enum CellState {
  Dead = "Dead",
  Alive = "Alive",
}
```
The game consists of a 2D grid where each slot is taken up by a cell. A cell can be either dead or alive. Now add the Cell class, ideally in another file
```typescript
import { CellState } from "./CellState";

export class Cell {
  public CurrentState: CellState = CellState.Dead;
  public NextState: CellState = CellState.Dead;

  constructor(currenCellState?: CellState) {
    if (currenCellState) {
      this.CurrentState = currenCellState;
    }
  }

  public tick = () => {
    this.CurrentState = this.NextState;
    this.NextState = CellState.Dead;
  };

  public toggle = () => {
    this.CurrentState = this.CurrentState === CellState.Alive ? CellState.Dead : CellState.Alive;
  };
}
```

The `CurrentState` of a Cell tells us how the cell is currently doing. Later we'll have to compute the new state of each Cell based on the state of its neighbors. To make the code simpler, I decided to store the next state of the Cell in the `NextState` property.
When the game is ready to transition each Cell into its next state, it can call `tick()` on the Cell instance and the `NextState` becomes the `CurrentState`.
The method `toggle()` will allow us to click somewhere on the 2D grid and kill or revive a Cell.

Let's talk about life. At the risk of sounding too reductionist, it's just a bunch of interacting cells. So we'll create one too
```typescript
import { Cell } from "./Cell";
import { CellState } from "./CellState";
import { EmptyCellsType } from "./EmptyCellsType";
import { InitialCellsType } from "./InitialCellsType";

export class Life {
  readonly columns: number;
  readonly rows: number;
  readonly onNewGeneration?: (newCells: ReadonlyArray<ReadonlyArray<Cell>>) => void;

  private _cells: Cell[][];

  public get cells(): Cell[][] {
    return this._cells;
  }

  constructor(input: InitialCellsType | EmptyCellsType, onNewGeneration?: (newCells: ReadonlyArray<ReadonlyArray<Cell>>) => void) {
    if (input instanceof InitialCellsType) {
      this._cells = input.initialCells;
      this.rows = this._cells.length;
      this.columns = this._cells[0].length;
    } else {
      this.columns = input.columns;
      this.rows = input.rows;
      if (this.columns <= 0 || this.rows <= 0) {
        throw new Error("Width and height must be greater than 0");
      }
      this._cells = [];
      for (let row: number = 0; row < this.rows; row++) {
        for (let col: number = 0; col < this.columns; col++) {
          this._cells[row] = this._cells[row] ?? [];
          this._cells[row][col] = new Cell(CellState.Dead);
        }
      }
    }

    this.onNewGeneration = onNewGeneration;
  }
}
```
Let's break down what we just created. Life is a class that keeps track of a bunch of cells. For that we're using `_cells:Cell[][]` which is just a 2D array of our simple `Cell` class. Having a 2D array allows us to know exactly where each cell is and who its neighbors are.
Traversing the 2D array can be cumbersome so I keep track of its dimensions with the properties `Rows` and `Columns`.

There are two ways in which I want to be able to create a new `Life`
* From scratch - meaning I jest tell it how many rows and columns of `Cell`s I want and the `Life` just initializes its 2D `_cells` array with `Cell`s in the `Dead` state.

For that, you need to add this new type
```typescript
export class EmptyCellsType {
  public columns: number = 0;
  public rows: number = 0;
}
```

It just holds a pair of numbers corresponding to the desired amount of `Cell` rows and columns.

* From a file - think of a saved game state. We'll later save the state of the game into a file and then load it up. When loading the saved game state, we need to tell the `Life` instance what each of its `Cell`'s state should be.
For now, just create this new class
```typescript
import { Cell } from "./Cell";

export class InitialCellsType {
  public initialCells: Cell[][] = [];
}
```

At this point we can create a new `Life`, where all the cells are either dead or in a state that we received from 'outside'.

Our `Life` needs a bit more functionality and then it is complete. The very first time we load up the game, all the cells will be dead. So it would be nice to be able to just breathe some life into the dead cells.
For that, `Life` needs a method that takes the location of a `Cell` and toggles its state to the opposite value.
```typescript
  public toggle = (row: number, col: number) => {
    if (row < 0 || row >= this.rows) {
      throw new Error("Row is out of range");
    }

    if (col < 0 || col >= this.rows) {
      throw new Error("Col is out of range");
    }

    const cellToToggle = this.cells[row][col];
    cellToToggle.toggle();
  };
```
The `Life` instance just makes sure that the specified location of the `Cell` makes sense and then tells that Cell to toggle its state. If you remember, the `Cell` class can toggle its state, if told to do so.

The last and most interesting method of `Life` implements the 3  [rules](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life#Rules) of the Game of Life.
1. Any live cell with two or three live neighbours survives.
2. Any dead cell with three live neighbours becomes a live cell.
3. All other live cells die in the next generation. Similarly, all other dead cells stay dead.

```typescript
  public tick = () => {
    // Compute the next state for each cell
    for (let row: number = 0; row < this.rows; row++) {
      for (let col: number = 0; col < this.columns; col++) {
        const currentCell = this._cells[row][col];
        const cellNeighbors = this.getNeighbors(row, col);
        const liveNeighbors = cellNeighbors.filter((neighbor) => neighbor.CurrentState === CellState.Alive).length;

        // Rule source - https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life#Rules
        if (currentCell.CurrentState === CellState.Alive && (liveNeighbors === 2 || liveNeighbors === 3)) {
          currentCell.NextState = CellState.Alive;
        } else if (currentCell.CurrentState === CellState.Dead && liveNeighbors === 3) {
          currentCell.NextState = CellState.Alive;
        } else {
          currentCell.NextState = CellState.Dead;
        }
      }
    }

    // Switch each cell to its next state
    for (let row: number = 0; row < this.rows; row++) {
      for (let col: number = 0; col < this.columns; col++) {
        const currentCell = this._cells[row][col];
        currentCell.tick();
      }
    }

    this.onNewGeneration?.(this.cells);
  };

  private getNeighbors = (row: number, col: number): Cell[] => {
    const neighbors: Cell[] = [];

    for (let colOffset: number = -1; colOffset <= 1; colOffset++) {
      for (let rowOffset: number = -1; rowOffset <= 1; rowOffset++) {
        if (colOffset === 0 && rowOffset === 0) {
          // skip self
          continue;
        }

        const neighborRow = row + rowOffset;
        const neighborCol = col + colOffset;
        if (neighborRow >= 0 && neighborRow < this.rows) {
          if (neighborCol >= 0 && neighborCol < this.columns) {
            neighbors.push(this._cells[neighborRow][neighborCol]);
          }
        }
      }
    }

    return neighbors;
  };
```
Let me quickly walk you through the code. I'm traversing the 2D array of `Cell`s, making use of the rows and columns. For each cell I'm looking at its neighbors and based on the 3 game rules I'm computing the next state of the `Cell`.
When I'm done with that, I'm traversing the 2D grid again (I know, not very efficient of me, but I went for readable code) and telling each `Cell` to switch to its next state.

You might be wondering what this `onNewGeneration()` function  is good for. Well, at this point in time I had no idea how the UI will function and I imagined that it would be nice to have a callback that lets me know when all the `Cell`s were updated to their new state. It just so happens that we don't need that callback after all.

We're done with the business logic. It's time for the UI.

## UI
In the `src` directory, create a new directory called `SimpleLifeComponent`. Inside this new directory create an `index.ts` file with this content
```typescript
export { SimpleLife } from "./simple-life.component";
```
Immediately after that, add a new file called `simple-life.component.tsx` next to the `index.ts` (this way VS Code will stop yelling at you that it can't find the referenced file).

#### KonvaJs
After some decent (10 minutes, but with my noise-cancelling headphones on) research (googled '2D drawing in React') of my own, I decided to go with [KonvaJs](https://www.konvajs.org).
It has excellent support for React. Take a look at this snippet from their [docs](https://konvajs.org/docs/react/Intro.html) and you'll be ready to draw in no time
```typescript
import { Stage, Layer, Rect, Circle } from 'react-konva';

export const App = () => {
  return (
    // Stage - is a div wrapper
    // Layer - is an actual 2d canvas element, so you can have several layers inside the stage
    // Rect and Circle are not DOM elements. They are 2d shapes on canvas
    <Stage width={window.innerWidth} height={window.innerHeight}>
      <Layer>
        <Rect width={50} height={50} fill="red" />
        <Circle x={200} y={200} stroke="black" radius={50} />
      </Layer>
    </Stage>
  );
}
```
So, all you have to do is install it like so
```bash
npm install react-konva konva
```

#### SimpleLife
This is going to be the component that takes care of rendering the game and it will allow us to interact with the game. As usual, it is possible to break up a React component in multiple smaller ones, but my intention was for YOU to see as much code as possible, at a glance.

Start by adding these imports
```typescript
import React, { FC, useCallback } from "react";
import { useState } from "react";
import { Layer, Stage, Rect } from "react-konva";
import { Cell } from "../models/Cell";
import { CellState } from "../models/CellState";
import { Life } from "../models/Life";
import { InitialCellsType } from "../models/InitialCellsType";
import { EmptyCellsType } from "../models/EmptyCellsType";
```

Nothing fancy here, just the normal React imports, *Konva* and our own types.

Next step is to add the props type
```typescript
interface Props {
  width: number;
  height: number;

  rows: number;
  columns: number;
}
```
The component will receive the number of rows and columns that define how many cells there's going to be. It also takes a width and a height, in pixels. The pixel dimensions tell our component how much space it has for its cells and it will 'fit' the cells in the available space. Don't overthink it, I didn't 😁.

We will need an instance of `Life` when the component lights up the very first time. For that, add this next function just below the `Props` interface
```typescript
function getInitialLife(columns: number, rows: number, onNewGeneration: (newCells: ReadonlyArray<ReadonlyArray<Cell>>) => void): Life | (() => Life) {
  return () => {
    const initialLife = new Life({ columns, rows } as EmptyCellsType, onNewGeneration);

    // Glider
    initialLife.toggle(2, 2);
    initialLife.toggle(3, 2);
    initialLife.toggle(4, 2);
    initialLife.toggle(4, 1);
    initialLife.toggle(3, 0);

    return initialLife;
  };
}
```
The function doesn't do much, but it's honest work. It takes the number of rows and columns (and that unused callback I mentioned above) and returns a function that returns a `Life` with the specified amount of rows and columns. It also toggles some of the `Cell`s to the `Alive` state. The shaped those live cells make is a [canonical shape] (https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life#Examples_of_patterns) and is called a 'Glider' because, as you will see, they will glide through the 2D space.

Add the `SimpleLife` component, below the previous function.
```typescript
export const SimpleLife: FC<Props> = ({ width, height, rows, columns }) => {
  const onNewGeneration = (newCells: ReadonlyArray<ReadonlyArray<Cell>>) => {
    // console.dir(newCells);
  };

  const [life, setLife] = useState<Life>(getInitialLife(columns, rows, onNewGeneration));
  const [, updateState] = useState({});
  const forceUpdate = useCallback(() => updateState({}), []);

  const onCellClicked = (row: number, column: number) => {
    life.toggle(row, column);
    forceUpdate();
  };

  const cellEdgeAndSpacingLength = Math.min(width / columns, (height - 30) / rows);
  const cellEdgeLength = 0.9 * cellEdgeAndSpacingLength;

  const canvasWidth = cellEdgeAndSpacingLength * columns;
  const canvasHeight = cellEdgeAndSpacingLength * rows;

  return (
    <>
      <Stage width={canvasWidth} height={canvasHeight}>
        <Layer>
          {life &&
            life.cells.map((cellRow, rowIndex) => {
              return cellRow.map((cell, columnIndex) => {
                return (
                  <Rect
                    key={(rowIndex + 1) * (columnIndex + 1)}
                    x={columnIndex * cellEdgeAndSpacingLength}
                    y={rowIndex * cellEdgeAndSpacingLength}
                    width={cellEdgeLength}
                    height={cellEdgeLength}
                    fill={cell.CurrentState === CellState.Alive ? "red" : "black"}
                    onClick={(e) => onCellClicked(rowIndex, columnIndex)}
                  ></Rect>
                );
              });
            })}
        </Layer>
      </Stage>
    </>
  );
};
```

Let's break it down. 
The component has a `Life` instance, which is its internal state. It is created with the `getInitialLife()` function that you added jus above the component. 
The `forceUpdate()` is just a little trick that allows us to force re-rendering.

Next up is the 4 lines with the computation. Their goal is to obtain the optimal cell edge length and canvas size, given the amount of rows and columns and the available space for our component.

Finally some TSX. Inside a `<Stage>`, which is a wrapper `<div>` for the canvas, I'm adding a `<Layer>` (Konva renders this as an HTML canvas) that contains many rectangles, one rectangle for each of our `Cell`s.

Remember that `life.cells` is an array of arrays of `Cell`. So there I'm using two nested calls to `map()` that allow me to traverse the whole data structure and emit a new Konva `<Rect>` for each `Cell`. 
`x` and `y` are the `<Rect>`'s pixel coordinates on the final canvas and `with` and `height` are the `<Rect>`'s pixel dimensions. A `<Rect>` will be ⬛️ when the `Cell` is dead and 🟥 when the `Cell` is alive. I've also wired up the `<Rect>`'s `onClick` handler to call our `onCellClicked()` function, which tells the `Life` instance to toggle the appropriate `Cell`'s state.

To actually see something on the screen, use the `<SimpleLife>` component in the `App.tsx` file. Something like this should work
```typescript
import React from "react";
import { SimpleLife } from "./SimpleLifeComponent";

function App() {
  return <SimpleLife width={window.innerWidth} 
                     height={window.innerHeight} 
                     rows={35} 
                     columns={35}></SimpleLife>;
}

export default App;
```
At this point you should be able to see the game and click cells to toggle their state.

##### It's alive!
Let's add a button that tells the `Life` instance to progress to the next generation of `Cell` states.
Back in the `SimpleLife` component, bellow `onCellClicked()`, add this function
```typescript
  const onTick = () => {
    life.tick();
    forceUpdate();
  };
```
And in the TSX, below the closing `Stage` tag (`</Stage>`) add this line
```TSX
<button onClick={() => onTick()}>Tick</button>
```

Now open the link with [canonical shapes](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life#Examples_of_patterns) in the Game of Life in a new browser window and create a few shapes by clicking in your game. By clicking the new button that you added, you should see how your shapes are doing in the Game of Life.

##### Oh my! 
Let's add a new button to clean up the mess you made :D
First add this new function below onTick()
```typescript
  const onClear = () => {
    setLife(getInitialLife(columns, rows, onNewGeneration));
  };
```

and this line of TSX below the previous button
```TSX
<button onClick={() => onClear()}>Clear</button>
```
Now you're able to clear the board and get the Glider back.

##### I'll save you, my little creatures, 4 ever!
"Wouldn't it be nice to be able to save the game state and reload it later?" I hear you ask. Excellent question and yes, that would be nice!

Let's start by preparing some infrastructure code. In your `src` directory, add a new one and call it `utils`. Inside utils create a file called `download.ts` and add this function
```typescript
export const download = (filename: string, text: string) => {
  var element = document.createElement("a");
  element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text));
  element.setAttribute("download", filename);

  element.style.display = "none";
  document.body.appendChild(element);

  element.click();

  document.body.removeChild(element);
};
```
The function takes a file name and some text, and tells your browser that it wants to save that text as a file with the specified name.

Back in the `SimpleLife` component, add this import
```typescript
import { download } from "./../utils/download";
```
Then add this function below `onClear()`
```typescript
  const onSave = () => {
    download(`game state ${new Date().toTimeString()}.json`, JSON.stringify(life.cells));
  };
```
And finally, add this button to the TSX, just below the other buttons
```TSX
<button onClick={() => onSave()}>Save</button>
```
Now, whenever you have an assortment of creatures that you're particularly fond of, you can save them as a JSON file.

"But how can I get them back?" Go back to `download.ts` and add this function
```typescript
export const pickFile = (onLoadedSuccessfully: (fileContent: string) => void) => {
  const filePickerInput = document.createElement("input");
  filePickerInput.type = "file";
  filePickerInput.id = "file";
  filePickerInput.className = "file-input";
  filePickerInput.accept = ".json";
  filePickerInput.style.display = "none";
  filePickerInput.onchange = (e) => {
    const filereader = new FileReader();
    filereader.onloadend = (ee) => {
      if (!ee) {
        return;
      }

      onLoadedSuccessfully(filereader.result as string);
    };
    filereader.readAsText((e.target as any)?.files?.[0]);
  };

  document.body.appendChild(filePickerInput);
  filePickerInput.click();
  document.body.removeChild(filePickerInput);
};
```
When invoked, it opens the browser's file picker dialog and lets your callback know whenever you pick a JSON file.
Back in `SimpleLife`, adjust the previous import to look like this
```typescript
import { download, pickFile } from "./../utils/download";
```

Now add this nasty little function, below `onSave()`
```typescript
  const onLoad = () => {
    pickFile((fileContent) => {
      const reloadedCellData = JSON.parse(fileContent);
      if (!reloadedCellData) {
        return;
      }

      const reloadedCellsMissingPrototypeChain = reloadedCellData as Array<Array<Cell>>;
      if (!reloadedCellsMissingPrototypeChain) {
        return;
      }

      const reconstructed: Cell[][] = [];
      const rows = reloadedCellsMissingPrototypeChain.length;
      const cols = reloadedCellsMissingPrototypeChain[0]?.length;

      for (let row: number = 0; row < rows; row++) {
        reconstructed[row] = reconstructed[row] ?? [];
        for (let col: number = 0; col < cols; col++) {
          reconstructed[row][col] = new Cell(reloadedCellsMissingPrototypeChain[row][col].CurrentState);
        }
      }

      const initialCell: InitialCellsType = new InitialCellsType();
      initialCell.initialCells = reconstructed;
      setLife(new Life(initialCell));
    });
  };
```
It triggers the file picker and when the right file is selected it will deserialize it into an instance of `Cell[][]`. Unfortunately, the deserialized object is lacking type information, which Typescript needs. So I'm just looping over the data and creating a proper `Cell[][]` instance.

finally, add yet another button to the TSX
```TSX
<button onClick={() => onLoad()}>Load</button>
```
And now you can load previous game states that you saved.

#### Conclusion
I had fun building this little game and I hope you had too. KonvaJs turned out to be an excellent little library and now I can't stop thinking about my next drawing adventure in React.

Keep your eyes open for new posts in this series. [Blazor](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor) should be next!