This is my first React project. I've included the main files that don't just contain boilerplate. This is a visualization of Conway's Game of Life, with a few toggleable features like starting/stopping and changing the speed.
Game.tsx
import React from "react"
import { Grid } from "./Grid";
import { randomProbability, wait } from "./utils";
type Props = {};
type State = {
grid: boolean[][]
running: boolean,
timeout: number
};
const HEIGHT = 30;
const WIDTH = 60;
const PROBABILITY_OF_ALIVE = 0.5;
export default class Game extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
grid: this.generateRandomGrid(),
running: false,
timeout: 10
};
}
async runSimulation() {
await this.setState({ running: true });
while (this.state.running) {
await this.simulateOneStep();
}
}
async simulateOneStep() {
const newGrid = this.nextGeneration();
await wait(this.state.timeout);
await this.setState({ grid: newGrid });
}
renderStopStartButtons() {
if (this.state.running) {
return (
<button className="menu-button" onClick={() => this.stop()}>
Stop
</button>
);
} else {
return (
<button className="menu-button" onClick={() => this.runSimulation()}>
Start
</button>
);
}
}
renderStepButton() {
if (this.state.running) {
return (
<button className="empty-menu-button">
Run One Step
</button>
);
} else {
return (
<button className="menu-button" onClick={() => this.simulateOneStep()}>
Run One Step
</button>
);
}
}
renderResetButton() {
return (
<button className="menu-button" onClick={() => window.location.reload()}>
Reset
</button>
)
}
renderSpeedSlider() {
return (
<span>
<label className="menu-text">Toggle Speed</label>
<input type="range" id="toggle-speed" min="5" max="1000"
onChange={event => this.setState({ timeout: parseInt(event.target.value) })}>
</input>
</span>
)
}
renderMenu() {
return (
<header>
<nav>
<ul id="menu">
<span className="menu-segment">
{this.renderResetButton()}
</span>
<span className="menu-segment">
{this.renderStopStartButtons()}
</span>
<span className="menu-segment">
{this.renderStepButton()}
</span>
<span className="menu-segment">
{this.renderSpeedSlider()}
</span>
</ul>
</nav>
</header>
)
}
stop() {
this.setState({ running: false });
}
start() {
this.setState({ running: true });
}
render() {
return (
<div id="page">
{this.renderMenu()}
<Grid dimensions={this.state.grid} />
</div>
)
}
generateRandomGrid() {
const grid: boolean[][] = [];
for (let row = 0; row < HEIGHT; row++) {
grid.push([]);
for (let col = 0; col < WIDTH; col++) {
grid[row].push(randomProbability(PROBABILITY_OF_ALIVE));
}
}
return grid;
}
nextGeneration() {
const newGrid = this.state.grid.slice();
for (let row = 0; row < HEIGHT; row++) {
for (let col = 0; col < WIDTH; col++) {
if (this.getAliveNeighbourCount(row, col) === 3) {
this.setAlive(newGrid, row, col);
} else if (this.isAlive(row, col) && this.getAliveNeighbourCount(row, col) === 2) {
this.setAlive(newGrid, row, col);
} else {
this.setDead(newGrid, row, col);
}
}
}
return newGrid;
}
setAlive(grid: boolean[][], row: number, col: number) {
grid[row][col] = true;
}
setDead(grid: boolean[][], row: number, col: number) {
grid[row][col] = false;
}
getAliveNeighbourCount(row: number, col: number) {
let neighbourCount = 0;
for (let i = -1; i < 2; i++) {
for (let j = -1; j < 2; j++) {
if (i === 0 && j === 0) {
continue;
}
if (this.isOnGrid(row + i, col + j) && this.isAlive(row + i, col + j)) {
neighbourCount++;
}
}
}
return neighbourCount;
}
isAlive(row: number, col: number) {
return this.state.grid[row][col];
}
isOnGrid(row: number, col: number) {
return row >= 0 && row < HEIGHT && col >= 0 && col < WIDTH;
}
}
Grid.tsx
import React from "react"
import { Tile } from "./Tile";
type Props = {
dimensions: boolean[][]
}
export const Grid = ({ dimensions }: Props) => {
const renderTile = (tile: boolean, index: number) => {
return <Tile isAlive={tile} key={index.toString()} />
}
const renderRow = (row: boolean[], index: number) => {
return (
<tr key={index.toString()}>
{row.map((tile, index) => {
return renderTile(tile, index);
})}
</tr>
)
}
return (
<table id="main-grid">
<tbody>
{dimensions.map((row, index) => {
return renderRow(row, index);
})}
</tbody>
</table>
);
}
Tile.tsx
import React from "react"
type Props = {
isAlive: boolean,
}
export const Tile = ({ isAlive }: Props) => {
const aliveStyle = isAlive ? "alive" : "";
return (
<td className={aliveStyle}>
</td>
);
}