ReactJS Training: Creating your first game with React and TypeScript
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.
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 withnpm 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:
- Present, explain, analyze and configure multiple tools ourselves (Webpack/Rollup, Babel/tsconfig, CSS Modules, etc.)
- 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:
Note: If
npx
doesn't work, try withnpm i -g create-react-app
and thencreate-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:
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.
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 withatom .
. And for Sublime, you can runsubl .
.
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:
And the browser displays a compilation error:
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 the
ReactDOM.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 therender()
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:
- We created a fully-functional web app with a single (npm) command in the Terminal.
- We executed the web application locally and displayed it in the browser.
- 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:
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.
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:
- It has to provide an
id
through the component’s props. - It could send a
chipType
to the component. As we mentioned above, Tiles can have a Chip or not. - 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 overthis.props
value in the first line of therender()
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 a
column
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
androws
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 ofPlayer
type:"red"
,"yellow"
or""
(representing an empty value). - The shape of the App’s
Props
andState
. The former tells us that we need to provide the number ofcolumns
androws
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:
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.
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"
orundefined
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.