31
loading...
This website collects cookies to deliver better user experience
npx create-react-app game-of-life --template typescript
App.tsx
to keep track of these values, including the grid itself. Store the grid in state so that it can be easily updated. For this we will employ the useState
hook. The useState
hook returns a stateful value, and a function to update it. Destructure those return values into grid
and setGrid
variables as shown below.// App.tsx
import { FC, useState } from "react";
const numRows = 25;
const numCols = 35;
const App: FC = () => {
const [grid, setGrid] = useState();
};
App.tsx
.useState
accepts one argument which will be returned as the initial state on the first render. Create a function that returns an array of random live and dead cells.// App.tsx
const randomTiles: = () => {
const rows = [];
for (let i = 0; i < numRows; i++) {
rows.push(Array.from(Array(numCols), () => (Math.random() > 0.7 ? 1 : 0))); // returns a live cell 70% of the time
}
return rows;
}
const App = () => {
const [grid, setGrid] = useState(() => {
return randomTiles();
});
};
randomTiles
function creates a multidimensional array of randomly placed 0s and 1s. 0 means dead and 1 means alive. The length of the array is the number of rows we declared earlier and each array in it contains numCols
items (in this case, 35). Notice that the type is annotated as an array of zeroes and ones. You can already see below what our grid will look like:// App.tsx
const App = () => {
const [grid, setGrid] = useState(() => {
return randomTiles();
});
return (
<div>
{grid.map((rows, i) =>
rows.map((col, k) => (
<div
style={{
width: 20,
height: 20,
backgroundColor: grid[i][k] ? "#F68E5F" : undefined,
border: "1px solid #595959",
}}
/>
))
)}
</div>
);
};
randomTiles
, and each time generates a 20 x 20 box to represent a cell. The background color of each cell is dependent on whether it is alive or dead.div
a Grid container and style it as follows:// App.tsx
<div
style={{
display: "grid",
gridTemplateColumns: `repeat(${numCols}, 20px)`,
width: "fit-content",
margin: "0 auto",
}}
>{...}</div>
//I use ... to denote code already established.
div
as follows:// App.tsx
return (
<div
style={
{
// ...
}
}
>
{grid.map((rows, i) =>
rows.map((col, k) => (
<div
key={`${i}-${k}`}
onClick={() => {
let newGrid = JSON.parse(JSON.stringify(grid));
newGrid[i][k] = grid[i][k] ? 0 : 1;
setGrid(newGrid);
}}
style={
{
// ...
}
}
></div>
))
)}
</div>
);
grid
array into a newGrid
,newGrid
.key
attribute of each cell to its specific position in the grid.false
. Let's allow TypeScript infer the type for us here which will be boolean
.// App.tsx
const App = () => {
const [grid, setGrid] = useState(() => {
return randomTiles();
});
const [running, setRunning] = useState(false);
// ...
};
// App.tsx
<button
onClick={() => {
setRunning(!running);
}}
>
{running ? "Stop" : "Start"}
</button>
positions
array outside the App component. This array represents the eight neighbors surrounding a cell, which we will make use of within the simulation.// App.tsx
import { useState, useCallback } from "react";
const positions = [
[0, 1],
[0, -1],
[1, -1],
[-1, 1],
[1, 1],
[-1, -1],
[1, 0],
[-1, 0],
];
runSimulation
using the useCallback
hook and pass the grid as an argument. The reason why useCallback
is being used here is to prevent our function from being created every time the App component is rendered. useCallback
creates a memoized function every time it's dependency array changes, this means that the function will be created only once and then run when necessary. In this case, we'll leave the dependency array empty.// App.tsx
const App = () => {
// ...
const runningRef = useRef(running);
runningRef.current = running;
const runSimulation = useCallback((grid) => {
if (!runningRef.current) {
return;
}
let gridCopy = JSON.parse(JSON.stringify(grid));
for (let i = 0; i < numRows; i++) {
for (let j = 0; j < numCols; j++) {
let neighbors = 0;
positions.forEach(([x, y]) => {
const newI = i + x;
const newJ = j + y;
if (newI >= 0 && newI < numRows && newJ >= 0 && newJ < numCols) {
neighbors += grid[newI][newJ];
}
});
if (neighbors < 2 || neighbors > 3) {
gridCopy[i][j] = 0;
} else if (grid[i][j] === 0 && neighbors === 3) {
gridCopy[i][j] = 1;
}
}
}
setGrid(gridCopy);
}, []);
// ...
};
runSimulation
once but we want the current running
value at all times, and the function will not keep updating the value for us. To fix that, let's create a runningRef
variable using the useRef
hook and initialize it to the current value of the running
state. This way, the running status is always up to date within our simulation because it is being stored in a ref. Whenever the .current
property of runningRef
is false, the function will stop, otherwise it will proceed to work with the rules of the game.runSimulation
clones the grid, loops over every cell in it and computes the live neighbors that each cell has by iterating over the positions
array. It then checks to make sure that we're not going out of bounds and are within the rows and columns in the grid. If that condition is met, it increments the number of live neighbors of the cell in question. The forEach
loop will run 8 times for each cell.neighbors
of the cell is less than 2 or greater than 3, the cell dies. Else, if the cell is dead and it has exactly 3 neighbors, the cell lives and proceeds to the next generation. After all the cells are covered, it updates the grid state with the gridCopy
.setInterval
method when the Start button is clicked:// App.tsx
<button
onClick={() => {
setRunning(!running);
if (!running) {
runningRef.current = true;
}
setInterval(() => {
runSimulation(grid);
}, 1000);
}}
>
{running ? "Stop" : "Start"}
</button>
runSimulation
every second. If you run this in your browser, you'll see that the simulation isn't running as it should. It appears to be stuck in a loop between two or three generations. This is due to the mismatch between the React programming model and setInterval
which you can read more about here. useInterval
. Create a file called useInterval.tsx
in your project directory and paste the following code into it:// useInterval.tsx
import { useEffect, useRef } from "react";
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
// Remember the latest callback if it changes.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
// Don't schedule if no delay is specified.
if (delay === null) {
return;
}
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
export default useInterval;
// App.tsx
import useInterval from "./useInterval";
// Put this right under runSimulation() inside the App function
useInterval(() => {
runSimulation(grid);
}, 150);
setInterval
, but works a bit differently. It’s more like setInterval
and clearInterval
tied in one, and it's arguments are dynamic. Delete the setInterval
function from the click handler and watch our app run smoothly.generateEmptyGrid
:// App.tsx
const generateEmptyGrid = (): number[][] => {
const rows = [];
for (let i = 0; i < numRows; i++) {
rows.push(Array.from(Array(numCols), () => 0));
}
return rows;
};
randomTiles
except it returns a multidimensional array containing only zeroes. Create a button to update the state with the new array of dead cells:// App.tsx
<button
onClick={() => {
setGrid(generateEmptyGrid());
}}
>
Clear board
</button>
grid
state, we initialized it to randomTiles
. Because we didn't annotate the type of randomTiles
, it's type was inferred as () => (0 | 1)[][]
, that is, a function that returns only zeroes and ones.generateEmptyGrid
's type is inferred as () => number[][]
which is not assignable to () => (0 | 1)[][]
. That is the reason behind that error above which shows that our code failed to compile. For our app to work, the types have to be compatible. Let's annotate their types so that they are the same:// App.tsx
const generateEmptyGrid = (): number[][] => {
const rows = [];
for (let i = 0; i < numRows; i++) {
rows.push(Array.from(Array(numCols), () => 0));
}
return rows;
};
const randomTiles = (): number[][] => {
const rows = [];
for (let i = 0; i < numRows; i++) {
rows.push(Array.from(Array(numCols), () => (Math.random() > 0.7 ? 1 : 0)));
}
return rows;
};
// App.tsx
<button
onClick={() => {
setGrid(randomTiles());
}}
>
Random
</button>
randomTiles
function that returns randomly placed 0s and 1s.useState
, useCallback
and useRef
. We saw how React and setInterval
don't work too well together and fixed the issue with a custom hook. We also discussed how TypeScript infers types when they are not annotated, how a type mismatch caused our code not to compile and how to solve the problem.