ReactJS Training: Creating your first game with React and TypeScript

Mariano Vazquez
JavaScript in Plain English
16 min readJan 20, 2020

--

Si quieres leer la versión en Español de este artículo, haz click aqui.

Now that we have covered the basics, it’s time to make things real. In this exercise, we will set up a React application from scratch, and then we will implement the Connect Four game on top of it.

This is the game we are going to build in this post: Connect four

By following this step-by-step walkthrough, you will learn about React and TypeScript while you code a real-life application. Are you ready? 👾

Initial setup

Note: If you want to start right away with the game logic, you can skip this section and open the begin application of this Exercise, located in this training’s GitHub repository. Remember to use npm install before running it with npm start.

As we explained in the previous post, neither TypeScript nor JSX runs in browsers. And since we are going to write code in these languages, we need to transpile it before executing our application. To do this, we have two options:

  1. Present, explain, analyze and configure multiple tools ourselves (Webpack/Rollup, Babel/tsconfig, CSS Modules, etc.)
  2. Take advantage of “scaffolders” (also named integrated toolchains), which are baked-apps that are already preconfigured and don’t require any extra setup to get started, letting us to only focus on our code.

In this post, we are going to go with the latter option, leveraging Facebook’s Create React app, which is the de-facto tool to build React applications nowadays.

And it’s super simple to set up:

In your terminal, run npx create-react-app connect-four --typescript. This command will create a TypeScript application inside the folder connect-four. Wait for the process to complete. You should see a message similar to this:

Create React app installation completed successfully message

Note: If npx doesn't work, try withnpm i -g create-react-app and then create-react-app connect-four --typescript.

Browse to the connect-four folder you’ve just created, and take a minute or two to analyze the folder structure. You are looking at a fully-functional application with the business logic inside the src folder:

connect-four
├── node_modules
│ ├── ...
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
│ └── ...
├── src
│ ├── App.css
│ ├── App.tsx
│ ├── index.css
│ ├── index.tsx
│ └── ...
├── package.json
├── tsconfig.json
└── ...

Now, run npm start.This command executes the app in “development mode”, which provides a lot of perks like automatic reloading when you make changes to the code (a.k.a. Hot Module replacement).

Open up http://localhost:3000 to visualize your application inside the browser:

Create React app initial Home screen

Congratulations! You’ve created your first application with React and TypeScript 👏 💃 🕺 👏

Let’s take it for a spin. Open the app with VSCode or the IDE of your preference and navigate to the src/App.tsx folder.

App.tsx file autogenerated by Create React app

Note: Pro tip! You can open VSCode pointing to the folder your terminal is by running code .. Similarly, you could do the same for Atom with atom .. And for Sublime, you can run subl ..

Take a couple of minutes to analyze the code in this file:

  • At the top, you have import statements. This is the way JavaScript (ES6) imports modules to a file. The value imported is stored in a variable for later use. You can learn more about ES6 imports here.
  • Line 5 defines a React Function component named App. It returns JSX code that will be later rendered by the browser (after the transpilation). React components help us to split our code into small pieces, following the single responsibility principle (or SRP).
  • Between lines 7 and 22 there is JSX code that represents what we see rendered in the browser. Notice that it is almost identical to HTML, except line 9 that sets up the src prop using a JavaScript variable reference (<img src={logo} .. />).
  • Last, line 26 exports our <App /> function and makes it available to be imported in other files.

With the app running locally (if you have stopped it, run npm start in your terminal), modify the code by removing the </div> closing tag in line 22, and save your changes.

Note that VSCode (or your IDE) now displays an error:

Error message displayed in VSCode

And the browser displays a compilation error:

Compilation error in the browser

Fix the error by undoing what you did (we removed the </div> closing tag), save your changes and wait for the browser to refresh your app.

Now, open the src/index.tsx file. This is the main entry point of the application:

import React from “react”;
import ReactDOM from “react-dom”;
import App from “./App”;
ReactDOM.render(<App />, document.getElementById(“root”));

The most important things you need to learn now are:

  • This file imports the <App /> component, along with the React and ReactDOM libraries.
  • It executes theReactDOM.render() method, passing as parameters the <App /> component and a reference to the element of the document (HTML) with "root” as ID.
  • You can find the HTML your browser renders when your app starts in the public/index.html file. In line 31, there is an empty <div id="root"> element. This is where your app will be "mounted", which means that this is where the application's code inside of the render() methods will be injected (as HTML).

This file has exactly the same content as the previous basic examples we saw in the previous post. This is the power of React: regardless of the complexity in your app, the code to render it is the same.

Wrapping up

In this quick walkthrough of the initial set up, we did the following:

  1. We created a fully-functional web app with a single (npm) command in the Terminal.
  2. We executed the web application locally and displayed it in the browser.
  3. We analyzed the React + TypeScript code of the app, both the main component and the main entry code, along with its HTML code.

Last, don’t forget that browsers only understand HTML, JS, and CSS, not TS or JSX. This scaffolded app has a build process that will generate JS and CSS files and will place it inside of a dist folder, and referenced in the index.html file, also generated. And these autogenerated files will be sent to the browser to parse, read, interpret and execute.

Note: If you are interested in reading a deep-dive explanation of the transpilation process, and how to configure tools for this, add a comment in this post and I’ll write it!

Adding the game logic to the app

Now that we understand the foundations of a React app, it’s time to add the game logic. As we explained in the previous post, React applications split the business logic into different components. But there are different responsibilities we’ll have to handle: the logic that decides who won (and if someone won), the logic that selects the elements to draw (and how), the one that determines whose player has the turn to make a move, etc. How can we split up these responsibilities in a consistent repeatable way?

We’ll use a widely-known pattern, Presentational and Container components, to organize our components in a simple, but yet powerful structure:

React data flows from “top to bottom”, while events bubble up information “from the bottom to the top”

This technique proposes to encapsulate all the business logic and state in Parent components (Container or Smart). And use their Children's components, usually the leaves, for rendering the UI and managing the user interaction (Presentational or Dumb components).

Container components send both data and functions to their children via props. Presentational components use the data to decide what and how to draw. And execute the functions when the user interacts with them, usually sending information as parameters.

Note: since your app’s will end up cascading the information from top to bottom, this approach is best suited for small/middle-sized apps. Large applications composed of a deeply nested hierarchy require a different approach. We’ll talk about it in a next post.

By following this technique, we can identify the following entities:

  • An App component, in charge of storing the state of the application. And to calculate who is the winner. It is the “parent”/”container”/”smart” component.
  • A Board component, responsible for drawing the elements of the game. The board is composed of multiple Columns composed of different Tiles that might or might not have a chip. They are the “child”/”presentational”/”dumb” components.
  • When a Column is clicked, a new Chip is added into an empty Tile at the bottom. This is part of the business logic of the app.
Split the application’s logic into small components

Of course, the components you use can vary depending on your preference. Can you think of a different way of organizing your code?

Creating a Tile component

Create a new folder named components inside of the src folder.

In this folder, create another folder named Tile, and inside it add the following (for now empty) files:

  • A Tile.module.css file to store the CSS code
  • A Tile.tsx file for the React’s component logic
  • And a types.ts file for the TypeScript types of the component.

Open up the src/components/Tile/types.ts file and paste the following code:

export interface Props {
id: string;
chipType?: string;
onClick: (id: string) => any;
}

By typing the Tile component’s props, we define its interface, or contract. It tells the component consumer that:

  1. It has to provide an id through the component’s props.
  2. It could send a chipType to the component. As we mentioned above, Tiles can have a Chip or not.
  3. It has to attach a function to the onClick prop, that will be triggered when the user clicks on a Tile.

Then, open the src/components/Tile.tsx file and paste the following code:

import React from "react";
import classNames from "classnames";
import styles from "./Tile.module.css";
import { Props } from "./types";
export default class Tile extends React.PureComponent<Props> {render() {
const { id, chipType, onClick = () => {} } = this.props;
const chipCssClass = classNames(styles.chip, chipType === "red" ? styles.red : styles.yellow);

return (
<div className={styles.tile} onClick={() => onClick(id)}>
{chipType && <div className={chipCssClass} />}
</div>
);
}
}

By looking at this code you’ll notice that the Tile component is a presentational component in charge of drawing tiles on your board. It decides if a Chip is present by checking the value of the chipType prop, and it sets the CSS class based on its value. Last, when clicked, it triggers the function set to the onClick prop, sending the Tile’s id as parameter.

Note: Have you noticed that we attached the Props interface to the React.PureComponent definition? This is how you type React class. The IDE will understand this and will tell you the type of each of the props in the compoennt. You can see this by hovering you your mouse over this.props value in the first line of the render() method. Give it a try!

Last, open the src/components/Tile.module.css file and paste this CSS code:

.tile {
width: 75px;
height: 75px;
border: solid 10px #3355ff;
border-radius: 100%;
background-color: white;
}
.chip {
width: 75px;
height: 75px;
border-radius: 100%;
background-color: gray;
}
.yellow {
background-color: #ffff33;
}
.red {
background-color: #ff010b;
}

Note: Create React app treats CSS files using [name].module.css differently from a regular CSS file, by transpiling them using the CSS Modules library. The main benefit of this is that you don’t need to worry about CSS class name clashing, as each file can be treated as an isolated module. This can be achieved because, when transpiling files, all CSS class names are replaced with a “unique” value of the format [filename]_[classname]__[hash].

For more information about this library, click here.

Creating a Column component

Now navigate to the components folder and create a new folder inside named Column.

Inside this folder, create the following files: a Column.module.css file to store the CSS code, a Column.tsx file for the React’s component logic and a types.ts file for the TypeScript types of the component.

Open up the src/components/Column/types.ts file and paste the following code that defines the props (contract) of the Column component:

import { ChipsPositions } from "../App/types";export interface Props {
column: number;
rows: number;
chipsPositions: ChipsPositions;
onTileClick: (id: string) => any;
}

This code tells the component’s consumer:

  • It needs to provide acolumn number. This value acts as the ID of the element.
  • It needs to define how many rows the Column component will have.
  • The chipsPositions prop is an object that knows the position of each chip. We will see how this object is built later. For now, you only need to know that it can tell us if there is a chip inside of a Tile or not.
  • Last, the onTileClick function is used to let the parent know when the user clicks on a specific tile.

Open up the src/components/Column.tsx file and paste the following code:

import React from "react";
import Tile from "../Tile/Tile";
import styles from "./Column.module.css";
import { Props } from "./types";
export default class Column extends React.PureComponent<Props> { render() {
const { column, rows, chipsPositions, onTileClick } = this.props;
const tiles = [];

for (let row = 0; row < rows; row++) {
const tileId = `${row}:${column}`;
const chipType = chipsPositions[tileId];
tiles.push(
<Tile
key={tileId}
id={tileId}
chipType={chipType}
onClick={onTileClick}
/>
);
}
return <div className={styles.column}>{tiles}</div>;
}
}

This (also presentational) code renders a <div> element containing as many Tile components as the rows value indicates (sent via props). Each tile will receive a chipType and the onTileClick() function. Notice that the unique tileId is defined here by combining the values of row and column.

Last, open the src/components/Column/Column.module.css file and paste the following CSS code:

.column {
display: flex;
flex-direction: column;
cursor: pointer;
}

We are almost there! 🙌

Creating a Board component

Similarly, navigate to the components folder and create a new folder inside named Board.

Inside this folder, create the following files: a Board.module.css file to store the CSS code, a Board.tsx file for the React’s component logic and a types.ts file for the TypeScript types of the component.

Note: Are you seeing a common pattern when creating components?

Open up the src/components/Board/types.ts file and paste the following code that defines the props (contract) of the Board component:

import { ChipsPositions } from “../App/types”;export interface Props {
columns: number;
rows: number;
chipsPositions: ChipsPositions;
onTileClick: (id: string) => any;
}

This code tells the component’s consumer that:

  • It has to provide the number of columns and rows the board will have.
  • It has to send the chipsPositions object. But this information is used by the Column component, not the Board.
  • It has to provide an onTileClick function, that will be used by the Tile component to signal when it is clicked.

Then, open the src/components/Board.tsx file and paste the following presentational code:

import React from "react";
import Column from "../Column/Column";
import styles from "./Board.module.css";
import { Props } from "./types";
export default class Board extends React.PureComponent<Props> {

renderColumns() {
const { columns, rows, chipsPositions, onTileClick } = this.props;
const columnsComponents = [];
for (let column = 0; column < columns; column++) {
columnsComponents.push(
<Column
key={column}
column={column}
rows={rows}
chipsPositions={chipsPositions}
onTileClick={onTileClick}
/>
);
}
return <>{columnsComponents}</>;
}
render() {
return <div className={styles.board}>{this.renderColumns()}</div>;
}
}

This code is similar to the Column component’s code, but instead of creating Tiles, we create multiple columns, passing the required information to them, and then we render the result. Thethis.renderColumns() method encapsulates this logic.

Have you noticed that we also use React.Fragment here? Probably not because we are using the shorthand “<></>”, which is an equivalent of “<React.Fragment></React.Fragment>”.

Last, open the src/components/Board/Board.module.css file and paste the following CSS code:

.board {
display: flex;
flex-direction: row;
border: solid 5px #002bff;
border-radius: 5px;
background-color: #3355ff;
}
.columns {
display: flex;
flex-direction: row;
}

Creating the App component

We are now going to develop the main logic for our game. Pay special attention to this section

Create a folder named App inside the src/components folder. Inside this folder, create the App.module.css file, the App.tsx file, and the types.ts file.

Open up the src/components/App/types.ts file and paste the following types:

export interface ChipsPositions {
[key: string]: Player;
}
export type Player = "red" | "yellow" | "";export interface Props {
columns: number;
rows: number;
}
export interface State {
chipsPositions: ChipsPositions;
gameStatus: string;
playerTurn: Player;
}

Here it is defined:

  • The shape of the ChipsPositions object: a dictionary containing in each position one of these values of Player type: "red", "yellow" or "" (representing an empty value).
  • The shape of the App’s Props and State. The former tells us that we need to provide the number of columns and rows for the App component to initialize, while the latter tells us all the information that will be stored by the component.

Now, open up the src/components/App/App.tsx and paste the following code:

import React from "react";
import Board from "../Board/Board";
import { Props, State, ChipsPositions } from "./types";
import styles from "./App.module.css";
export default class App extends React.PureComponent<Props, State> {
state: State = {
chipsPositions: {},
playerTurn: "red",
gameStatus: "It's red's turn"
};
calculateGameStatus = (playerTurn: string, chipsPositions: ChipsPositions): string => {
// TODO
};
handleTileClick = (tileId: string) => {
// TODO
};
renderBoard() {
const { columns, rows } = this.props;
const { chipsPositions } = this.state;
return (
<Board
columns={columns}
rows={rows}
chipsPositions={chipsPositions}
onTileClick={this.handleTileClick}
/>
);
}
renderStatusMessage() {
const { gameStatus } = this.state;
return <div className={styles.statusMessage}>{gameStatus}</div>;
}
render() {
return (
<div className={styles.app}>
{this.renderBoard()}
{this.renderStatusMessage()}
</div>
);
}
}

This is the basic structure of the component: presentational logic to draw/render the Board and the Status message, and a default App’s state. This code is completely functional, but the app still won’t react if the user interacts with the game. We’ll code this logic in the next few lines.

Implement the handleTileClick() method to react when the user clicks on a Tile.

handleTileClick = (tileId: string) => {
const { chipsPositions, playerTurn } = this.state;
// Get the last empty tile of the column
const column = parseInt(tileId.split(":")[1]);
let lastEmptyTileId = this.getLastEmptyTile(column);
// If there is no empty tile in the column, do nothing
if (!lastEmptyTileId) {
return;
}
// Add chip to empty tile
const newChipsPositions = {
...chipsPositions,
[lastEmptyTileId]: playerTurn
};
// Change player turn
const newPlayerTurn = playerTurn === "red" ? "yellow" : "red";
// Calculate game status
const gameStatus = this.calculateGameStatus(newPlayerTurn, newChipsPositions);
// Save new state
this.setState({ chipsPositions: newChipsPositions, playerTurn: newPlayerTurn, gameStatus });
};
getLastEmptyTile(column: number) {
const { rows } = this.props;
const { chipsPositions } = this.state;
for (let row = rows - 1; row >= 0; row--) {
const tileId = `${row}:${column}`;

if (!chipsPositions[tileId]) {
return tileId;
}
}
}

Take a couple of minutes to understand what the code does:

  • First, it needs the last empty Tile of the column that was clicked. And it gets the column number by parsing the tileId.
  • Then, it adds a chip to the selected tile depending on the player’s turn, known by the App component alone. And it recalculates the game status.
  • Last, it stores all the new information in the component’s state, re-rendering the entire application if something changes. React will decide this for us.

Last, implement the calculateGameStatus() method by pasting the following code inside the App component. The code contains the logic that decides who the winner is, or who plays next:

calculateGameStatus = (playerTurn: string, chipsPositions: ChipsPositions): string => {
const { columns, rows } = this.props;
// Check four in a row horizontally
for (let row = 0; row < rows; row++) {
let repetitionCountStatus = { playerChip: "", count: 0 };
for (let column = 0; column < columns; column++) {
const chip = chipsPositions[`${row}:${column}`];

// If there is a chip in that position, and belongs
// to a player, count that chip for that player
// (either increase the count or start over)
if (chip && chip === repetitionCountStatus.playerChip) {
repetitionCountStatus.count++;
} else {
repetitionCountStatus = { playerChip: chip, count: 1 };
}
// If the count for a player is 4, that player won
if (repetitionCountStatus.count === 4) {
return `Player ${repetitionCountStatus.playerChip} won!`;
}
}
}
// Check four in a row vertically
for (let column = 0; column < columns; column++) {
let repetitionCountStatus = { playerChip: "", count: 0 };

for (let row = 0; row < rows; row++) {
const chip = chipsPositions[`${row}:${column}`];
// If there is a chip in that position, and belongs
// to a player, count that chip for that player
// (either increase the count or start over)
if (chip && chip === repetitionCountStatus.playerChip) {
repetitionCountStatus.count++;
} else {
repetitionCountStatus = { playerChip: chip, count: 1 };
}
// If the count for a player is 4, that player won
if (repetitionCountStatus.count === 4) {
return `Player ${repetitionCountStatus.playerChip} won!`;
}
}
}
// TODO: Check four in a row diagonally

return `It's ${playerTurn}'s turn`;
};

Did you notice that this code does not check for four consecutive chips of the same value in diagonal? Can you come up with an implementation for this? If you do, send it to me as a Pull Request!

Initializing the app

Open up the src/index.tsx file and replace its content with the following code:

import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
import "./index.css";
// Initialize the app with 7 columns and 6 rows
ReactDOM.render(
<App columns={7} rows={6} />,
document.getElementById("root")
);

Start the app by running npm start in a terminal.

In the newly opened browser window, open the Developer Console, and then click the Components tab. You will see the hierarchy tree of your React application, composed of the components you’ve just created:

Connect Four app hierarchy tree in the Developer Console

Play with the game a little bit, add a few chips into the board, and then check the different Tiles of the board in the Developer Console. Notice that the properties received changed after you interacted with them.

When you click on an empty tile, its chipType changes

Note: You can also change a prop directly by modifying its value in the right panel. Try it yourself by turning a Tile’s chip type from "red" or undefined to "yellow".

Congratulations! You have just created your first game with React and TypeScript 💪💪💪

Wrapping up

In this exercise, we learned the following:

  • How to create an application from scratch using React and TypeScript.
  • How to split your app’s business logic into small components.
  • How to send information and notify user events via props.
  • How to use the React Developer tools to visualize your application’s component tree and its state.

🎉🎉

Remember that you can find the full training in this GitHub repository.

--

--