Cellular Automata Using React

Bryan Moon
The Startup
Published in
6 min readDec 4, 2020

Recently I decided to build John Conway’s game of life to help me gain a deeper understanding of React and some of it’s functionality. The game of life is an example of cellular automata — a simulation wherein each cell on a 2-D grid possess an internal state. Cellular automations rely on a ruleset that dictate the behaviors of each cell depending on its neighboring cells and its own previous state.

To get started with this project we first need to build a grid that will serve as our environment. We can do this by first setting the number of rows and columns we want our environment to have, and then iterate through them to create an array of arrays wherein the element in each row will either be a 0 or a 1 to represent the state of that particular cell. This make’s each cell’s ‘state’ easy to refer to being that an array of arrays will give each cell a coordinate describing their position within the grid.

numRows = 5numCols = 5resetGrid = () => {    rows = []    for (let i = 0;i < numRows; i++) {        rows.push(Array.from(Array(numCols),() => 0))    };return rows;}

The Array.from() method needs the ‘obj’ you are creating an array from as it’s argument, but it can take in some additional arguments, one being a map function. The second argument you see there is map callback function that is giving each cell their initial value of 0.

The next thing would be to represent our array of arrays as html elements in the browser. We want our react app to remember the state of the entire grid itself so to achieve this we will implement the useState hook and set our create grid function to the initial value of the state we want. Then, similarly to how the grid itself was created, we can iterate through our grid and display each element as a div like so.

const [grid, setGrid] = useState(() => {    return resetGrid()});return (<><div  style={{    display: 'grid',    gridTemplateColumns: `repeat(${numCols}, 10px)`,  }}>  {grid.map((rows, x) =>    rows.map((col, y) => (      <div        onClick={() => {          const newGrid = Produce(grid, (gridCopy) => {            gridCopy[x][y] = grid[x][y] ? 0 : 1;        });        setGrid(newGrid);      }}      key={`${x}-${y}`}      style={{        width: 10,        height: 10,        backgroundColor: grid[x][y] ? 'red' : undefined,        border: 'solid 1px black',    }}/>)))}  </div></>)}

The useState hook is a tool used to manage state in a functional component, and functional components are preferred because they are easier to parse and you can avoid the use of ‘this’ within the component. The useState hook above uses destructuring to show the state defined as grid and its setState function defined as setGrid. You can also see that rather than just passing in the ‘resetGrid()’ function directly into the useState hook, we are passing it in through a callback function. This ensures that our grid is only created once rather than every time the component renders. We use a callback function when the state we are setting can potentially slow down our app and consume too much memory because it will only be called upon initializing.

You may also notice this other function called Produce(). Produce is a wonderful function imported from a package called ‘immer’ and it is responsible for creating an immutable copy for our ‘next state’. Because we don’t want to alter state directly, we need a copy of our current state that we can alter and use for the next state of our grid. That function will fire off when that particular div at is clicked and change the value of the element in the corresponding coordinates. Because ‘0’ and ‘1’ return falsey or truthy values, we can use that to set up our ternary statement that will toggle it from it’s previous value, which controls the background color for that div.

The next step is writing out the logic for the actual simulation. The rules for this simulation are as follows:

  1. Any live cell with fewer than two live neighbours dies, as if by underpopulation.
  2. Any live cell with two or three live neighbours lives on to the next generation.
  3. Any live cell with more than three live neighbours dies, as if by overpopulation.
  4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.

We can express this logic like so:

if (neighbors < 2 || neighbors > 3) {    gridCopy[x][y] = 0;} else if (grid[x][y] === 0 && neighbors === 3) {    gridCopy[x][y] = 1;}

If we can express the rules like this, all we would need to do for this work is figure out a way to count the neighbors of each cell. In this simulation, we define a neighbor as any cell immediately around the current cell. Each red cell pictured here can be referred to from the blue cell with coordinates. We can then create a list of coordinates in relation to the blue cell to navigate to those cells and in order to determine their current state.

const neighborCoordinates = [[0, 1],[0, -1],[1, 1],[1, -1],[-1, 0],[-1, 1],[-1, -1],[1, 0]]
const [running, setRunning] = useState(false)
const runSimulation = useCallback(() => { if (!running) { return; } setGrid((grid) => { return Produce(grid, gridCopy => { for (let x = 0; x < numRows; x++) { for (let y = 0; y < numCols; y++) { let neighbors = 0; neighborCoordinates.forEach(([a,b]) => { const newX = x + a; const newY = y + b; if (newX >= 0 && newX < numRows && newY >= 0 && newY < numCols) { neighbors += grid[newX][newY] } })// "game of life - rules" if (neighbors < 2 || neighbors > 3) { gridCopy[x][y] = 0; } else if (grid[x][y] === 0 && neighbors === 3) { gridCopy[x][y] = 1; }}}});});setTimeout(runSimulation, 300);}, []);

The react hooks useRef, and useCallback are very helpful in this portion of our game. We use the useCallback function here because we want our app to only create this function once and we know that this function will never really change. By passing our entire simulation function into the useCallback hook, we are telling our app to only render when the values passed into its dependencies array changes. The array all the way at the bottom can hold variables that the rendering of this function will rely on. So, if you were to pass in a value that changes, the function would re-render as it depends on that value. We are going to pass an array with nothing in it so that the function will only render once because we don’t need this function to re-render, it will work the same way every time it’s called.

There is a problem here we need to fix however, because if this function is only created one time, then it will never update the boolean value we are storing in the useState running hook. While we are making efforts to write more efficient code, we will need more ways to reference variables that might change in value over time. To solve this problem, we can use the useRef hook and store the current value of running in our reference.

const [running, setRunning] = useState(false)const runningRef = useRef(running);runningRef.current = runningconst runSimulation = useCallback(() => {  if (!runningRef.current) {return;}.
.
.

By storing this value in a useRef hook, we can allow other parts of our program to reference the value without depending on that value and causing any other function that might need it to re-render. This makes our code more efficient because it will only look for the current value of a single variable, kind of like how React treats the rendering of dom elements in reference to the virtual dom, only the components that change get re-rendered.

Now that we have all the pieces in place, we can put this code to the test and observe the behaviors of our cells.

I used the following resources as references to this blog article. Big thanks to Ben Awad for helping gain a deeper understanding to hooks in React.

--

--