How to make Life with React Hooks

The developer’s task is to show the user how digital cells live and die. The author took advantage of React and its hooks: state management and the ability to abstract from state-related logic make the project easy to read and understand. We share the implementation details and code on Github while we start Frontend development course


Rules of the game

The universe of the game is an infinite two-dimensional orthogonal grid of square cells, each of which is in one of two possible states: alive or dead (or inhabited and unpopulated, respectively). Each cell interacts with its eight neighbors – cells that are located side by side horizontally, vertically or diagonally. At each step in time, the following transitions occur:

  • Any living cell with fewer than two living neighbors dies of overpopulation.

  • Any living cell with two or three living neighbors is passed on to the next generation.

  • Any living cell with more than three living neighbors dies as if overpopulated.

  • Any dead cell with three living neighbors becomes a living cell, as if reproducing.

Try it my app, and then let’s talk about how it works under the hood.

The data structure I decided to use to represent the cells is pretty simple. Here is an array of objects:

This array of objects represents our grid, and each object represents a separate cell.  The alive property is key in the game
This array of objects represents our grid, and each object represents a separate cell. The alive property is key in the game

We create a Grid display component, it is superimposed on the grid array and generates an individual cell for each object in the grid array:

gridSize stores the size of the grid. I have three default sizes: 15×15, 30×30, or 50×50. Different sizes will have different styles. Let’s take a look at the helper functions:

Helper function dynamically changes the amount of space allocated to each column and row of the grid
Helper function dynamically changes the amount of space allocated to each column and row of the grid
The function in cellSize returns the width and height of an individual cell based on the gridSize.  The function in cellDisplay creates 3 random colors and then checks if the passed cell is alive or dead.  If it is live, it dynamically sets the size of the cell and then gives it a random color;  if dead, then dynamically sets the cell size and sets a black background
The function in cellSize returns the width and height of an individual cell based on the gridSize. The function in cellDisplay creates 3 random colors and then checks if the passed cell is alive or dead. If it is live, it dynamically sets the size of the cell and then gives it a random color; if dead, then dynamically sets the cell size and sets a black background

Now let’s look at the logic for changing the cell depending on the generation and how the game controls are connected. All the logic related to state, as well as how we manage the appearance of the grid in a particular generation, is handled in a custom hook. useGrid

useGrid contains multiple calls useState to track the information we use to both iterate generations and control the game:

State for tracking information that we will use to iterate over generations and to control the game
State for tracking information that we will use to iterate over generations and to control the game

First, you need to find out if the grid has a combination of cells that you can change. The corresponding logic is located in a helper function stepThroughAutomata inside useGrid… I started to draw up a plan for the function using George Poya’s problem solving methods.

StepThroughAutomata development plan
StepThroughAutomata development plan

We bring the plan to life:

Step 1. Set a variable to determine if the cell is valid (rule-based mutation is possible)
Step 1. Set a variable to determine if the cell is valid (rule-based mutation is possible)
Step 2. Apply a mapping to the current grid, save the result as nextGeneration, and use the getNeighbors helper function to check the neighbors of the current cell
Step 2. Apply a mapping to the current grid, save the result as nextGeneration, and use the getNeighbors helper function to check the neighbors of the current cell
Step 3. Initialize the livingNeighbors value to 0, then check all the neighbors of the current cell to see if they are alive.  For each living neighbor, increase the livingNeighbors value by 1
Step 3. Initialize the livingNeighbors value to 0, then check all the neighbors of the current cell to see if they are alive. For each living neighbor, increase the livingNeighbors value by 1
Step 4. Based on the number of living neighbors of the current cell, check the game rules and switch the current cell to live if it was dead, to dead if it was alive, or to unchanged.  Set validGrid to true if mutation is done: if possible, then the grid is valid
Step 4. Based on the number of living neighbors of the current cell, check the game rules and switch the current cell to live if it was dead, to dead if it was alive, or to unchanged. Set validGrid to true if mutation is done: if possible, then the grid is valid
Step 5. If the grid is valid, increase the value of the generation counter;  otherwise, inform the user that the grid is invalid.  Finally, set the grid as the next generation.  The next generation will be the old grid, but with rule-based changes
Step 5. If the grid is valid, increase the value of the generation counter; otherwise, inform the user that the grid is invalid. Finally, set the grid as the next generation. The next generation will be the old grid, but with rule-based changes

And it’s all! Let’s go to management.

Control
Control

So the first button here is Step 1 Generation.

Implementing the button is pretty straightforward: we have a function stepThroughAutomata… And below we see the component Controls

On line 13, we have the first button. We just add the property onClick to this button and pass to it stepThroughAutomata:

On line 22, we have an input field that defines the iteration rate.

And finally, there is a third button, the value of which is “Start” or “Stop” depending on whether individual cells are clickable. If the cells are clickable, then the game is running. If not, the game is not running.

You might ask, “Wait a second, when I press the start button, the function stepThroughAutomata does not start? ” Yes! JS method setInterval doesn’t work very well with onClick… Therefore, for this functionality, you had to create your own hook. Let’s see how it works:

GridContainer: black box with magic
GridContainer: black box with magic

Above, we are destructuring all data from useGrid, but right below this code, we call another custom hook – useInterval with four parameters. It:

  1. Callback function (here – stepThroughAutomata).

  2. The time between calls to the passed function, in milliseconds. Meaning speedInput the default is 500.

  3. Current grid.

  4. Boolean value, here clickable

useInterval and hook magic.
useInterval and hook magic.

We created a hook useInterval because the built-in setInterval function doesn’t always go well with how React re-renders components.

We need a way to know that the mesh is changing, and therefore the mesh needs to be re-rendered, and we need to make sure that it changes consistently every n milliseconds. We can find out with the help of the built-in hook useRef… First we initialize savedCallback as a link.

Now let’s use useEffectto set the current savedCallback as the passed callback. It has to do with how setInterval subscribes to an object window and unsubscribes from him.

We will update savedCallback.current every time the return value of the callback changes. This should happen every time the callback function is executed.

The second call to useEffect is inside useInterval.
The second call to useEffect is inside useInterval.

Moving on to the second challenge useEffect… Let’s first check if it is true clickable… If so, then you don’t want to run the function inside: such a launch means that the game is running right now. If clickable – false, this means that the game is being launched for the first time. Therefore, we quickly initialize the function tickwhich calls the currently stored callback.

Save the call result setIntervalpassing tick and delay, and then immediately unsubscribe using an anonymous function and doing clearInterval with passing id

The great thing is that the passed callback is the same function we use to iterate over one generation at a time, so the iteration algorithm is completely reusable.

Summary:

  • Hooks allow you to write clean code that can be reused.

  • Defining neighbors by flattening the mesh into a vector and performing mathematical operations to find neighbors gives spatial complexity O(n)

  • React’s built-in re-render function allows you to create a seamless UI representation of a Life game.

Code on Github

You can continue learning ReactJS in our courses:

Other professions and courses

Data Science and Machine Learning

Python, web development

Mobile development

Java and C #

From the basics to the depth

And:

Similar Posts

Leave a Reply