Building a Chess Game with React
Published: 2023-05-23 23:38:26

by [Judge Godwins](https://blog.openreplay.com/authors/judge-godwins)

<blockquote><em>Want to play chess? Want to learn React tricks? Want to use sockets for communication? This article will answer all three questions by building a multiplayer chess game using React and Socket.io.</em></blockquote>



Chess is a timeless classic that has been enjoyed by millions of people around the world for centuries. With the rise of online gaming, it's now possible to play chess with anyone, anywhere, at any time. In this article, we'll explore how to build a multiplayer chess game using [chess.js](https://github.com/jhlywa/chess.js/blob/master/README.md), [react-chessboard](https://www.npmjs.com/package/react-chessboard), and [socket.io](https://socket.io/). These powerful libraries will allow us to create a fast and responsive chess game with real-time multiplayer functionality. So, whether you're an experienced chess player or just looking for a fun project to work on, let's dive in and build our very own multiplayer chess game! 

Here's a breakdown of the libraries that will be used in this project:
* [react-chessboard](https://www.npmjs.com/package/react-chessboard): For rendering the chess board.
* [chess.js](https://github.com/jhlywa/chess.js/blob/master/README.md) library: For move generation and validation.
* [socket.io](https://socket.io): for websocket connection and multiplayer connectivity.
* [Material UI](https://mui.com): For building the app UI.

## Project Structure
In this project, we'll make use of two directories. One for the client application and the other for the server. Here's what the project directory structure looks like:

```
multiplayer-chess
|--client
|--server
```

Create the `client` and `server` directories. You can find the code for this project [here](https://github.com/judgegodwins/multiplayer-chess).

## Building the Client Application
Let's create a new React app in the `client` directory. To do this, change into the `client` directory and run the following:
```bash
npx create-react-app .
```
Next, let's install the required dependencies for the client. In the client directory, run the following:
```bash
npm install react-chessboard chess.js socket.io-client @mui/material @emotion/react @emotion/styled

```
[`@mui/material`](https://www.npmjs.com/package/@mui/material), [`@emotion/react`](https://www.npmjs.com/package/@emotion/react) and [`@emotion/styled`](https://www.npmjs.com/package/@emotion/styled) are required to setup material UI.

After installing the required dependencies, delete all files in the `src` directory **except `index.js` and `App.js`**. Edit the `index.js` file to contain the following:
```javascript
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
```

Also, let's edit the `App.js` file to contain the following:
```javascript
import { Container } from "@mui/material";
import Game from "./Game";

export default function App() {
  return (
    <Container>
      <Game />
    </Container>
  );
}
```
If you noticed, we are importing a Game.js file that hasn't been created yet. This `Game` component will contain the core logic of our application. 

We'll create the `Game` component, but before that, let's create a reusable modal/dialog component (based on Material UI) that will be used in various sections of the app. Create a new directory called `components`, then create a new file called `CustomDialog.js` in the `components` directory. Add the following content to CustomDialog.js:

`src/components/CustomDialog.js`
```javascript
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";

export default function CustomDialog({ open, children, title, contentText, handleContinue }) {
  return (
    <Dialog open={open}> {/*dialog container*/}
      <DialogTitle>{title}</DialogTitle>
      <DialogContent> {/* Main body of modal/dialog */}
        <DialogContentText> {/* main text */}
          {contentText}
        </DialogContentText>
        {children} {/* Other content */}
      </DialogContent>
      <DialogActions> {/* Dialog action buttons */}
        {/* Force users to make input without option to cancel */}
        {/* <Button onClick={handleClose}>Cancel</Button> */}
        <Button onClick={handleContinue}>Continue</Button>
      </DialogActions>
    </Dialog>
  );
}
```
The props of the `CustomDialog` component include:
* `open`: Boolean value to determine if the dialog should be rendered or not.
* `children`: Prop to get the component's children. Children elements are rendered in the dialog content.
* `title`: Title of the dialog.
* `contentText`: Message to be displayed in the dialog.
* `handleContinue`: Function to be called when the "continue button" is clicked.

Now, let's create the Game component. Create a `Game.js` file in the `src` directory and add the following content:
```javascript
import { useState, useMemo, useCallback, useEffect } from "react";
import { Chessboard } from "react-chessboard";
import { Chess } from "chess.js";
import CustomDialog from "./components/CustomDialog";

function Game({ players, room, orientation, cleanup }) {
  const chess = useMemo(() => new Chess(), []); // <- 1
  const [fen, setFen] = useState(chess.fen()); // <- 2
  const [over, setOver] = useState("");

  // onDrop function
  function onDrop() {} // <- 3
  
  // Game component returned jsx
  return (
    <>
      <div className="board">
        <Chessboard position={fen} onPieceDrop={onDrop} />  {/**  <- 4 */}
      </div>
      <CustomDialog // <- 5
        open={Boolean(over)}
        title={over}
        contentText={over}
        handleContinue={() => {
          setOver("");
        }}
      />
    </>
  );
}

export default Game;
```
Below is a breakdown of the code above. Notice the line-number comments in the code above; each line-number comment corresponds to a numbered line below.
1. We created a memoized `Chess` instance with 0 dependencies. The `useMemo` hook lets us cache the chess instance between re-renders so that the instance is not created on every re-render. This `Chess` instance will be used for move validation and generation.
2. We set the initial `fen` state to the FEN (Forsyth-Edwards Notation) returned from the `Chess` instance. FEN is a standard notation to describe the positions of a chess game.
3. We created an empty `onDrop` function
4. We set up the chessboard using the `Chessboard` component from `react-chessboard` and passed the FEN notation (`fen`) to the `position` prop, as well as passed the `onDrop` function to the `onPieceDrop prop`. The `onPieceDrop` function prop is called every time a piece is moved.
6. We created a dialog component that will be rendered when the state `over` is truthy. `over` will contain a text that describes the reason the game is over. This may be a checkmate or a stalemate/draw.

You can start the app by running `npm start` on your terminal (in the `client` directory). You'll find a chessboard displayed.

![-](https://blog.openreplay.com/images/building-a-chess-game-with-react/images/NntgKgb.png)

We are not yet handling piece movements. Let's fix that. Let's complete the `onDrop` function in the `Game` component:
```javascript
  // ...
  // onDrop function
  function onDrop(sourceSquare, targetSquare) {
    const moveData = {
      from: sourceSquare,
      to: targetSquare,
      color: chess.turn(),
      // promotion: "q",
    };

    const move = makeAMove(moveData);

    // illegal move
    if (move === null) return false;

    return true;
  }
  // ...
```
In the code above, we created the `onDrop` function that receives two function parameters `sourceSquare` (the initial piece position) and `targetSquare`(target position). Inside the function, we constructed a `moveData` object using `sourceSquare`, `targetSquare`, and `color`, which is set to `chess.turn()` (`chess.turn()` returns the color of the current side set to play, either ``'b'`` or ``'w'``). Then we call `makeAMove`(not defined yet). This function will pass `moveData` to the `Chess` instance for validation and generation. We return true or false depending on the returned value of `makeAMove`.

Now let's create the `makeAMove` function. Add the following to the `Game` component, just above the `onDrop` function.
```javascript
// ...
const makeAMove = useCallback(
  (move) => {
    try {
      const result = chess.move(move); // update Chess instance
      setFen(chess.fen()); // update fen state to trigger a re-render

      console.log("over, checkmate", chess.isGameOver(), chess.isCheckmate());

      if (chess.isGameOver()) { // check if move led to "game over"
        if (chess.isCheckmate()) { // if reason for game over is a checkmate
          // Set message to checkmate. 
          setOver(
            `Checkmate! ${chess.turn() === "w" ? "black" : "white"} wins!`
          ); 
          // The winner is determined by checking which side made the last move
        } else if (chess.isDraw()) { // if it is a draw
          setOver("Draw"); // set message to "Draw"
        } else {
          setOver("Game over");
        }
      }

      return result;
    } catch (e) {
      return null;
    } // null if the move was illegal, the move object if the move was legal
  },
  [chess]
);
// ...
```
In the code above:
* We created the `makeAMove` function using a `useCallback` hook with `chess` as a dependency in order to cache the function definition between re-renders and avoid creating the function on every re-render. 
* The `makeAMove` function accepts a move and calls `chess.move` with a `move` object as an argument. This method (`.move`) validates the move and updates the `Chess` instance's internal state. Next, we set the `Game` component's `fen` state to reflect that of the `Chess` instance. This triggers a re-render and updates the chessboard.
* After the move is made, we check if it resulted in a 'game over'. If this is true, we determine if it was due to a checkmate or a draw and update the game's state accordingly with an appropriate message.
* The `makeAMove` function body is wrapped in a try...catch block because calling `chess.move` with an illegal move object throws an error. When an error is thrown, we simply return `null`. The null return value is handled in the `onDrop` function, as discussed earlier.

You can open the app in your browser and play as both sides. Ensure that the app is running by executing `npm start` in the `client` directory, and try to achieve a checkmate.

![-](https://blog.openreplay.com/images/building-a-chess-game-with-react/images/OAn6Ys5.gif)

We have developed a web-based chess application that allows two players to play against each other on the same computer. Now, it's time to set up the backend server to enable multiplayer connectivity and allow players to compete with each other over the internet.


## Setting up the Backend
To enable multiplayer connectivity, we'll need a backend server. Let's create an HTTP and websocket server using ExpressJS and Socket.io.

- Express is a minimal and flexible Node.js web application framework that offers a comprehensive set of features for building web applications. With Express, you can effortlessly set up an HTTP server in a Node.js environment.

- Socket.io is a framework that offers a straightforward API for creating a WebSocket server to facilitate WebSocket connections. WebSocket connections provide a two-way communication channel between the client and server, where both sides can send and receive messages within a single TCP connection. WebSocket connections are established over HTTP connections through a process of upgrading the connection.

First, let's initialize the project and install its dependencies. Open a terminal in the `server` directory and execute the following command:

```bash
npm init -y
```

```bash
npm install express socket.io uuid
```

Install Nodemon as a development dependency. Nodemon automatically restarts the server whenever changes are made. Execute the following command in the terminal:

```bash
npm install nodemon --save-dev
```

To use Nodemon in the `npm start` command, include the start command in the `package.json` file as follows:"

```json
{
  "scripts": {
    "start": "nodemon server.js"
  }
}
```

Create a new file named `server.js` in the server directory, and add the following code to set up an Express and Socket.io server:

```javascript
const express = require('express');
const { Server } = require("socket.io");
const { v4: uuidV4 } = require('uuid');
const http = require('http');

const app = express(); // initialize express

const server = http.createServer(app);


// set port to value received from environment variable or 8080 if null
const port = process.env.PORT || 8080 

// upgrade http server to websocket server
const io = new Server(server, {
  cors: '*', // allow connection from any origin
});

// io.connection

server.listen(port, () => {
  console.log(`listening on *:${port}`);
});
```
The code above initializes an Express server and creates a WebSocket server using the socket.io library. Here's a breakdown of the code above:

* The necessary libraries (`express`, `socket.io`, `http` and [`uuid`](https://www.npmjs.com/package/uuid)) are imported.
* An Express application is initialized by calling the `express()` function, then, an HTTP server is created using the `http.createServer()` function with the express application (`app`) passed as an argument.
* The code sets the port to a value received from an environment variable using `process.env.PORT` or defaults to `8080` if the environment variable is not set.
* Next, it creates a WebSocket server by calling the `new Server()` constructor function from the socket.io library, passing in the HTTP server, and setting the `cors` option to allow connections from any origin.
* Finally, the server listens on the specified port using the `server.listen()` function and logs a message to the console to indicate that it is listening for incoming connections.

After setting up the HTTP and WebSocket servers, we need to listen for incoming WebSocket connections on the WebSocket server. Add the following listener to server.js, just below the `io` server definition:

```javascript
// io.connection
io.on('connection', (socket) => {
  // socket refers to the client socket that just got connected.
  // each socket is assigned an id
  console.log(socket.id, 'connected');
});
```
Run `npm start` in the terminal to start the server. You can also run `node server.js`, but this will not restart the server whenever changes are made. You should see a message `listening on *:8080`.

## Implementing Multiplayer Functionality
To establish a WebSocket connection on the client, we need to enable it. We previously installed the `socket.io-client` package, and we'll use the `io` function from this package to connect to the Socket.io server.

Switch to the `client` directory. In the `src` folder, create a new file called `socket.js` and add the following content:
```javascript
import { io } from "socket.io-client"; // import connection function

const socket = io('localhost:8080'); // initialize websocket connection

export default socket;
```
From the code above, the `io` function is imported from `socket.io-client`. Then, the `io` function is called with the server address as a function argument, which initiates the WebSocket connection. 

Each connected user needs to have an associated username. For simplicity, we will obtain a user's desired username whenever the app is launched by making use of a dialog that contains a text field to accept user input. 

When a username has been selected, the username retrieved from the user will be stored in a state in the `App` component. Then, a WebSocket event named `username` carrying the selected username as event data will be emitted to the server via the WebSocket connection. When the `username` event is received on the backend server, the username which was sent as event data will be attached to the client's socket data.

Edit `client/src/App.js` to contain the following content:
```javascript
import { useState } from "react";
import { Container, TextField } from "@mui/material";
import Game from "./Game";
import socket from './socket';
import CustomDialog from "./components/CustomDialog";

export default function App() {
  const [username, setUsername] = useState('');

  // indicates if a username has been submitted
  const [usernameSubmitted, setUsernameSubmitted] = useState(false);

  return (
    <Container>
      <CustomDialog
        open={!usernameSubmitted} // leave open if username has not been selected
        title="Pick a username" // Title of dialog
        contentText="Please select a username" // content text of dialog
        handleContinue={() => { // fired when continue is clicked
          if (!username) return; // if username hasn't been entered, do nothing
          socket.emit("username", username); // emit a websocket event called "username" with the username as data
          setUsernameSubmitted(true); // indicate that username has been submitted
        }}
      >
        <TextField // Input
          autoFocus // automatically set focus on input (make it active).
          margin="dense"
          id="username"
          label="Username"
          name="username"
          value={username}
          required
          onChange={(e) => setUsername(e.target.value)} // update username state with value
          type="text"
          fullWidth
          variant="standard"
        />
      </CustomDialog>
      <Game />
    </Container>
  );
}
```
Go through the comments for an explanation of what's going on.

Make sure the backend server is already running (run `npm start` in the `server` directory on your terminal). Start the client app (if it isn't running) by executing `npm start` in the `client` directory on your terminal. On your browser, you should see the dialog popup asking for a username.

![-](https://blog.openreplay.com/images/building-a-chess-game-with-react/images/KzakQjz.png)

You should also see a message in the server's logs similar to this:

```bash
Q9uuNsmiSDiHMOCkAAAC connected
```
The first item in the log message is the socket ID. You can see this log because the `socket.js` has been imported into `App.js` and a WebSocket connection has been initiated. 

Now we have to add a WebSocket event listener on the backend to listen to the `username` event and retrieve the username data from the client. This data will be attached to the client's socket instance.

In the `server` directory, open `server.js` and add the socket event listener inside the callback function of `io.on('connection')` as shown below:

`server/server.js`
```javascript
// ...
// io.connection
io.on('connection', (socket) => {
  // socket refers to the client socket that just got connected.
  // each socket is assigned an id
  console.log(socket.id, 'connected');
  // listen to username event

  socket.on('username', (username) => {
    console.log('username:', username);
    socket.data.username = username;
  });
});
// ...
```
When you enter a username in the dialog box on the client app and click on 'continue', the server should receive the "username" event and data. You'll find a log in the terminal similar to this:

```bash
username: judge
```

## Initializing a Game
Let's now create a component that will allow users to start or join a game. We'll be making use of the concept of rooms. Think of it as a literal room that users can join and leave. When there are users in a room, any message emitted to that room will be received by all users in that room. 

In our application, the two players in a game will be in a socket room. Events emitted to that room will be received by both players.

Let's create a new component called `InitGame`. This component will be responsible for initializing a game. Through this component, a user can either create or join a game room.  Create a new file called `InitGame.js` in the client's `src` directory and add the following:

```javascript
import { Button, Stack, TextField } from "@mui/material";
import { useState } from "react";
import CustomDialog from "./components/CustomDialog";
import socket from './socket';

export default function InitGame({ setRoom, setOrientation, setPlayers }) {
  const [roomDialogOpen, setRoomDialogOpen] = useState(false);
  const [roomInput, setRoomInput] = useState(''); // input state
  const [roomError, setRoomError] = useState('');

  return (
    <Stack
      justifyContent="center"
      alignItems="center"
      sx={{ py: 1, height: "100vh" }}
    >
      <CustomDialog
        open={roomDialogOpen}
        handleClose={() => setRoomDialogOpen(false)}
        title="Select Room to Join"
        contentText="Enter a valid room ID to join the room"
        handleContinue={() => {
          // join a room
        }}
      >
        <TextField
          autoFocus
          margin="dense"
          id="room"
          label="Room ID"
          name="room"
          value={roomInput}
          required
          onChange={(e) => setRoomInput(e.target.value)}
          type="text"
          fullWidth
          variant="standard"
          error={Boolean(roomError)}
          helperText={!roomError ? 'Enter a room ID' : `Invalid room ID: ${roomError}` }
        />
      </CustomDialog>
      {/* Button for starting a game */}
      <Button
        variant="contained"
        onClick={() => {
          // create a room
        }}
      >
        Start a game
      </Button>
      {/* Button for joining a game */}
      <Button
        onClick={() => {
          setRoomDialogOpen(true)
        }}
      >
        Join a game
      </Button>
    </Stack>
  );
}
```
Explanation of the code above:
* We created a new component called `InitGame`.
* `InitGame` has three states:
    * `roomDialogOpen`:  This is a boolean state that determines if the `CustomDialog` should be rendered. The dialog contains a text field that allows the user to enter the ID of a room they'd like to join. 
    * `roomInput`: This state enables the component to control the text input. It will contain the room ID the user has provided.
    * `roomError`: This state keeps track of whatever error was encountered while trying to join a room.
* Material UI's Stack component is used to wrap other elements. A stack component is simply a css flex component with a default `flex-direction` property of `'column'`
* We created a dialog that contains a text field that'll enable a user to enter a room ID they'd like to join.
* There are two action buttons. One to start a new game and another to join a game using the room ID. The button to join a game simply causes the dialog to pop up. All "room-joining" actions will be performed in the dialog
* The props (`setRoom`, `setOrientation`, `setPlayers`) are functions to update the app state. We'll see how these work.


<div style="background-color:#efefef; border-radius:8px; padding:10px; display:block;">
<hr/>
<h3><em>Session Replay for Developers</em></h3> 
<p><em>Uncover frustrations, understand bugs and fix slowdowns like never before with <strong><a href="https://github.com/openreplay/openreplay" target="_blank">OpenReplay</a></strong> — an open-source session replay suite for developers. It can be <strong>self-hosted</strong> in minutes, giving you complete control over your customer data.</em></p>
<img alt="OpenReplay" style="margin-top:5px; margin-bottom:5px;" width="768" height="400" src="https://raw.githubusercontent.com/openreplay/openreplay/main/static/openreplay-git-hero.svg" class="astro-UXNKDZ4E" loading="lazy" decoding="async">
<p><em>Happy debugging! <a href="https://openreplay.com" target="_blank">Try using OpenReplay today.</a></em><p>
<hr/>
</div>


## Creating and Joining Game Rooms

To implement room creation on the client side, we need to update the `onclick` function of the button that starts a game. This function will emit a WebSocket event called `createRoom`. We will also update the socket.io server to listen to this event and create a new room.

Update the `onclick` prop of the "Start a game" button in the `InitGame` component, as shown below:
```javascript
<Button
  variant="contained"
  onClick={() => {
    socket.emit("createRoom", (r) => {
      console.log(r);
      setRoom(r);
      setOrientation("white");
    });
  }}
>
  Start a game
</Button>
```
In the `onclick` function, we emit a `createRoom` event through the WebSocket connection, and this time, we make use of a callback. We use a callback because it will be called on the backend and passed a `roomId` argument. This callback function is a way of receiving a response from the server. The value of the response `r` will be the room's ID. In the callback function, we update the room state (by calling `setRoom`) with the value of the room ID and also set the orientation/side to white (so a player who creates a room plays the white side by default).


On the server, we have to listen to this `createRoom` event and handle the creation of rooms.

Before adding an event listener for the `createRoom` event, we need a data structure to store the rooms' data. A room's data will include a room ID and a list of players in the room. For simplicity, we'll be making use of a [Javascript Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map). If you intend to build for production, consider making use of a database like Redis to store room data and game state.

In the `server/server.js` file, create a new `rooms` constant above the `io.on('connection')` function and assign it a `Map` object.

```javascript
// ...
const rooms = new Map();

// io.on('connection')
// ...
```

Then, update the callback function of `io.on('connection')` by adding a new `createRoom` event listener just below the ``username`` event listener, as shown below:

```javascript
// io.on('connection');
io.on('connection', (socket) => {
  console.log(socket.id, 'connected');

  // socket.on('username')
  socket.on('username', (username) => {
    console.log(username);  
    socket.data.username = username;
  });

  // createRoom
  socket.on('createRoom', async (callback) => { // callback here refers to the callback function from the client passed as data
    const roomId = uuidV4(); // <- 1 create a new uuid
    await socket.join(roomId); // <- 2 make creating user join the room
   
    // set roomId as a key and roomData including players as value in the map
    rooms.set(roomId, { // <- 3
      roomId,
      players: [{ id: socket.id, username: socket.data?.username }]
    });
    // returns Map(1){'2b5b51a9-707b-42d6-9da8-dc19f863c0d0' => [{id: 'socketid', username: 'username1'}]}

    callback(roomId); // <- 4 respond with roomId to client by calling the callback function from the client
  });
});
```
Each numbered line below maps to a line-number comment in the code above. In the `createRoom` event listener:
1. A new room ID is created using the `uuid` package.
2. The player creating the room joins the room.
3. A key-value pair is added to the rooms map, with the uuid as key and room data as value. The room data contains the room Id and list of players.
4. The `callback` function passed as data from the client (as shown previously in the `onclick` function of the "continue" button of the `CustomDialog`) is received in the listener function on the server. This callback is called at the end with the newly created `roomId` passed as a parameter. When the `callback` is called, the client receives the roomId as a response from the server.

To implement room joins, the client application has to emit a socket.io event named `joinRoom` that will be handled by the server. This event's data will include the ID of the room the player intends to join. 

As outlined earlier, on the client-side, the socket.io client needs to emit a `joinRoom` event to the server when a user tries to join a room. Update the `handleContinue` prop of the `CustomDialog` element in the `InitGame` component, as shown below:

`client/src/InitGame.js`
```javascript
// ...
<CustomDialog
  // ...
  handleContinue={() => {
    // join a room
    if (!roomInput) return; // if given room input is valid, do nothing.
    socket.emit("joinRoom", { roomId: roomInput }, (r) => {
      // r is the response from the server
      if (r.error) return setRoomError(r.message); // if an error is returned in the response set roomError to the error message and exit
      console.log("response:", r);
      setRoom(r?.roomId); // set room to the room ID
      setPlayers(r?.players); // set players array to the array of players in the room
      setOrientation("black"); // set orientation as black
      setRoomDialogOpen(false); // close dialog
    });
  }}
>
  {/** ... */}
</CustomDialog>
```
From the code snippet above:
* a socket.io event called `joinRoom` is emitted along with the room ID when a user clicks on the 'continue' button in the dialog.
* a callback function is also used to receive a response from the server.
* we set the `room` and `players` states to the respective data returned from the callback, and we also set the orientation to "black".

On the server side, we need to add a listener to listen to a `joinRoom` event and perform actions necessary to join a room. In the listener, we need to check that a room exists, has a player waiting, and is not full. If any of these conditions are not met, we return an error to the user indicating that the room ID is invalid.

The complete code for this implementation is available in the `InitGame.js` file located [here](https://github.com/judgegodwins/multiplayer-chess/blob/before-disconnect/client/src/InitGame.js).

In the `server/server.js` file, add a new `joinRoom` event listener in the `io.on('connection')` callback:
```javascript
io.on('connection', (socket) => {
  // ...
  socket.on('joinRoom', async (args, callback) => {
    // check if room exists and has a player waiting
    const room = rooms.get(args.roomId);
    let error, message;
  
    if (!room) { // if room does not exist
      error = true;
      message = 'room does not exist';
    } else if (room.length <= 0) { // if room is empty set appropriate message
      error = true;
      message = 'room is empty';
    } else if (room.length >= 2) { // if room is full
      error = true;
      message = 'room is full'; // set message to 'room is full'
    }

    if (error) {
      // if there's an error, check if the client passed a callback,
      // call the callback (if it exists) with an error object and exit or 
      // just exit if the callback is not given

      if (callback) { // if user passed a callback, call it with an error payload
        callback({
          error,
          message
        });
      }

      return; // exit
    }

    await socket.join(args.roomId); // make the joining client join the room

    // add the joining user's data to the list of players in the room
    const roomUpdate = {
      ...room,
      players: [
        ...room.players,
        { id: socket.id, username: socket.data?.username },
      ],
    };

    rooms.set(args.roomId, roomUpdate);

    callback(roomUpdate); // respond to the client with the room details.

    // emit an 'opponentJoined' event to the room to tell the other player that an opponent has joined
    socket.to(args.roomId).emit('opponentJoined', roomUpdate);
  });
});
```
Be sure to read the comments in the code to gain a clearer understanding of what's going on. 

The code snippet above includes a validation step to confirm the room ID's validity. If the ID is valid, the "joining player" is added to the room, and a response containing the room ID and the list of players in the room is sent back to the "joining player" using the callback function. Additionally, we notify the other player in the room that an opponent has joined, indicating that the game is ready to begin. If the room ID is invalid, we respond with an error message. This error message is handled in the callback function passed to the `socket.emit` call in the `handleContinue` prop of the room-joining dialog in the `InitGame` component, as shown earlier.

Now, we need to update the `App` component in `App.js` to render the `Game` component only when a room has been created or joined by the user, otherwise, the `InitGame` component is rendered so that the user can choose to create or join a game room. 

Modify the App component to resemble the following code:
```javascript
import { useEffect, useState, useCallback } from "react";
import Container from "@mui/material/Container";
import Game from "./Game";
import InitGame from "./InitGame";
import CustomDialog from "./components/CustomDialog";
import socket from "./socket";
import { TextField } from "@mui/material";

export default function App() {
  const [username, setUsername] = useState("");
  const [usernameSubmitted, setUsernameSubmitted] = useState(false);

  const [room, setRoom] = useState("");
  const [orientation, setOrientation] = useState("");
  const [players, setPlayers] = useState([]);

  // resets the states responsible for initializing a game
  const cleanup = useCallback(() => {
    setRoom("");
    setOrientation("");
    setPlayers("");
  }, []);

  useEffect(() => {
    // const username = prompt("Username");
    // setUsername(username);
    // socket.emit("username", username);

    socket.on("opponentJoined", (roomData) => {
      console.log("roomData", roomData)
      setPlayers(roomData.players);
    });
  }, []);

  return (
    <Container>
      <CustomDialog
        open={!usernameSubmitted}
        handleClose={() => setUsernameSubmitted(true)}
        title="Pick a username"
        contentText="Please select a username"
        handleContinue={() => {
          if (!username) return;
          socket.emit("username", username);
          setUsernameSubmitted(true);
        }}
      >
        <TextField
          autoFocus
          margin="dense"
          id="username"
          label="Username"
          name="username"
          value={username}
          required
          onChange={(e) => setUsername(e.target.value)}
          type="text"
          fullWidth
          variant="standard"
        />
      </CustomDialog>
      {room ? (
        <Game
          room={room}
          orientation={orientation}
          username={username}
          players={players}
          // the cleanup function will be used by Game to reset the state when a game is over
          cleanup={cleanup}
        />
      ) : (
        <InitGame
          setRoom={setRoom}
          setOrientation={setOrientation}
          setPlayers={setPlayers}
        />
      )}
    </Container>
  );
}
```
Notice the following:
* We added three new states (`room`, `players` and `orientation`)
    * `room`: stores the current room ID
    * `players`: stores the players in a room
    * `orientation`: stores the board orientation for a user.
* These 3 new states are updated in the `InitGame` component as shown earlier when a user joins or creates a game room.
* The `cleanup` function resets the three states mentioned above which are responsible for initializing a game. This function will be used in the `Game` component when the game is over.
* The `Game` component is now conditionally rendered only when a room has been created or joined (i.e when the `room` state holds a truthy value).
* The `InitGame` component is rendered if a room has not yet been created or joined, allowing a user to create or join a game room. It receives three functions (`setRoom`, `setOrientation`, and `setPlayers`) as props, which are used to update the `room`, `orientation`, and `players` states, respectively, when a user creates or joins a game.
* Since WebSocket connections allow 2-way communication between the server and client, we added a socket.io event listener in the `useEffect` hook that listens to the `opponentJoined` event. As discussed earlier, the server emits the `opponentJoined` event to a player when an opponent has joined a room they created. This event comes with the room data (including the list of players in the room), so we simply set the `players` state to the value of `roomData.players` received from the event data.

To launch the app on your browser, ensure that the backend server is running on `localhost:8080`. Enter a username and click on the "Start Game" button to trigger the `createRoom` event, which will generate a room ID. You will find the room ID logged in the browser console. To join the same room from a different browser tab, open a new tab, enter a different username, and click on "Join a Game". Next, copy and paste the room ID from the console in the previous tab and click on "Continue". Upon joining the room, you will see the list of players logged on the console of the first tab. This list is retrieved from the "opponentJoined" event that is received by the room creator when an opponent joins the game room.

![-](https://blog.openreplay.com/images/building-a-chess-game-with-react/images/uHIgqpP.gif)

## Handling Piece Movements
Whenever a player (let's say player-1) makes a move, our application should send a `move` event to the player on the other end (player-2) to reflect the moves made by player-1. The `Game` component will handle the logic for transmitting `move` events.

Whenever a piece is moved, the `onDrop` function in the `Game` component is triggered. This function currently takes in two arguments (`sourceSquare` and `targetSquare`). It creates a `move` object using these arguments and then calls the `makeAMove` function (with the `move` object as a parameter), which passes the `move` object to the Chess.js instance for validation and state update. To inform the other player of a move, we will need to emit a socket event named `move`. This event will contain the move data. Once the other player receives this event, the `makeAMove` function will be called on their end to update the app's state.

Before we update the `onDrop` function, let's import the socket.io client into `Game.js`. Add the following import statement at the top of the `Game.js` file:

```javascript
import socket from "./socket";
```
Let's update the `onDrop` function in the `Game` component

`Game.js`
```javascript
  // onDrop function
  function onDrop(sourceSquare, targetSquare) {
    // orientation is either 'white' or 'black'. game.turn() returns 'w' or 'b'
    if (chess.turn() !== orientation[0]) return false; // <- 1 prohibit player from moving piece of other player

    if (players.length < 2) return false; // <- 2 disallow a move if the opponent has not joined

    const moveData = {
      from: sourceSquare,
      to: targetSquare,
      color: chess.turn(),
      promotion: "q", // promote to queen where possible
    };

    const move = makeAMove(moveData);

    // illegal move
    if (move === null) return false;

    socket.emit("move", { // <- 3 emit a move event.
      move,
      room,
    }); // this event will be transmitted to the opponent via the server

    return true;
  }
```
Each numbered line below maps to a line-number comment in the code above:

1. A check is made to ensure that a player cannot move the chess pieces of their opponents. For example, a player on the white side should not be able to move black pieces.
2. A check is made to ensure that an opponent is connected before making a move.
3. After a move is validated and played, an event called `move` is emitted to the game room along with the move data. This move data would be emitted to the opponent's client app.

How does the opponent receive the move data emitted by the first player? To accomplish this, we must utilize socket.io event listeners in both the backend and the client.

Let's set up the backend server to listen to `move` event and transmit it to the second player.

In the `server.js` file, add a new `move` event listener function in the `io.on("connection")` callback function:

```javascript
// ...
io.on('connection', (socket) => {
  // ...
  socket.on('move', (data) => {
    // emit to all sockets in the room except the emitting socket.
    socket.to(data.room).emit('move', data.move);
  });
});
```
In the code above, when a `move` event is received on the backend, it is broadcasted to all sockets in the room except the socket that generated the event. So if player-1 and player-2 are in the same game room and player-1 emits a `move` event, every socket except player-1's socket connection will receive the `move` event, in this case, only player-2's client receives the `move` event.

Here's a visual representation of the event flow:

![-](https://blog.openreplay.com/images/building-a-chess-game-with-react/images/1sQnXvi.png)

When the backend server emits the `move` event to player-2's client,  player-2's client needs to listen to this event and play the move on behalf of player-1. Therefore, we need to set up an event listener on the `Game` component. The listener function will be triggered whenever the opponent makes a move. We'll set up the listener in a useEffect hook so it'll be registered when the component is mounted.

Add the following `useEffect` hook to the `Game` component in `Game.js` below the `onDrop` function:

```javascript
  useEffect(() => {
    socket.on("move", (move) => {
      makeAMove(move); //
    });
  }, [makeAMove]);
```
In the code above, we set up the socket.io event listener when the component is mounted (i.e, in the `useEffect` hook). When a client receive's a `move` event, the move is validated and played using the `makeAMove` function.

In the Game's UI, we need to display the game room's ID and list of players. 

Add the following at the top of `Game.js` to import UI components from Material UI:

```javascript
import {
  Card,
  CardContent,
  List,
  ListItem,
  ListItemText,
  ListSubheader,
  Stack,
  Typography,
  Box,
} from "@mui/material";
```

Update the returned JSX in the `Game` component to display the room ID and players:

```javascript
// Game component returned jsx
return (
  <Stack>
    <Card>
      <CardContent>
        <Typography variant="h5">Room ID: {room}</Typography>
      </CardContent>
    </Card>
    <Stack flexDirection="row" sx={{ pt: 2 }}>
      <div className="board" style={{
        maxWidth: 600,
        maxHeight: 600,
        flexGrow: 1,
      }}>
        <Chessboard
          position={fen}
          onPieceDrop={onDrop}
          boardOrientation={orientation}
        />
      </div>
      {players.length > 0 && (
        <Box>
          <List>
            <ListSubheader>Players</ListSubheader>
            {players.map((p) => (
              <ListItem key={p.id}>
                <ListItemText primary={p.username} />
              </ListItem>
            ))}
          </List>
        </Box>
      )}
    </Stack>
    <CustomDialog // Game Over CustomDialog
      open={Boolean(over)}
      title={over}
      contentText={over}
      handleContinue={() => {
        setOver("");
      }}
    />
  </Stack>
);
```

Here's a breakdown of what's happening:

* The code is returning a React component that renders a stack of elements using the `<Stack>` component provided by Material UI.

* The first element in the stack is a `<Card>` component with a `<Typography>` component inside of it that displays the room ID. `Typography` is a Material UI component used for rendering text.

* The second element in the stack is a horizontal stack (`flexDirection="row`) created with another `<Stack>` component. It contains a `<div>` element with a `className` of `board` that sets some CSS styles to specify the size of the board.

* Notice the new prop in the `Chessboard` component called `boardOrientation`. This determines the orientation of the chessboard for a player, which could be either white or black. Players who create a room play on the white side by default, while players who join a room play on the black side.

* The second element in the horizontal stack also conditionally renders a `<Box>` component that contains a `<List>` of player names. This only occurs if the players array has at least one item.

* The third element in the stack is a custom dialog box (`<CustomDialog>`) that displays when the `over` state variable is truthy. It contains a title, some content text, and a button that clears the over state when clicked.

Overall, the code above renders a chess game interface with a board and player list, and a custom dialog for when the game is over.

You can view the entire content of `Game.js` at this point [here](https://github.com/judgegodwins/multiplayer-chess/blob/before-disconnect/client/src/Game.js)

Two players can now play against each other provided they are in the same room. Launch the app (make sure the server is up and running the coommand `npm start` on the server directory), open two browser tabs, create and join a room on each tab. Try playing both sides to get a checkmate.

![-](https://blog.openreplay.com/images/building-a-chess-game-with-react/images/4W50YCr.gif)

## Handling Disconnections
When a player gets disconnected while in a game room, the other player in the room has to be informed that the opponent has been disconnected. In a production app, you'll want to implement some reconnection logic to allow disconnected clients to automatically rejoin a game room after they have reconnected. More information on this can be found in the "Conclusion" section of this article. For simplicity, when a client disconnects, we'll just inform the other player that the opponent has been disconnected and then close the game room.

How does the server know when a client has been disconnected? Socket.io emits a special event called `disconnect` from the client to the server whenever a client gets disconnected. We can set up a `disconnect` event listener in the server and emit a `playerDisconnected` event to all rooms that the client has joined. When this event is emitted to a room, the other player in the room will receive this event and will be notified of the opponent's disconnection.

Let's add the `disconnect` event listener. In the `server.js` file, add a new `disconnect` event listener in the `io.on("connection")` callback function just below the `move` event listener.

```javascript
// io.connection
io.on("connection", (socket) => {
  // ...
  
  socket.on("disconnect", () => {
    const gameRooms = Array.from(rooms.values()); // <- 1

    gameRooms.forEach((room) => { // <- 2
      const userInRoom = room.players.find((player) => player.id === socket.id); // <- 3

      if (userInRoom) {
        if (room.players.length < 2) {
          // if there's only 1 player in the room, close it and exit.
          rooms.delete(room.roomId);
          return;
        }

        socket.to(room.roomId).emit("playerDisconnected", userInRoom); // <- 4
      }
    });
  });
});
```
In the code above, we added a `disconnect` event listener. Below is a breakdown of what's happening in the listener's callback function. Notice the line number comments in the code above; each numbered line below corresponds to a line-number comment in the code above.

1. The list of available rooms can be retrieved and stored in the `gameRooms` variable. To obtain an array of all values within a map, we made use the `.values()` method. Specifically, calling `rooms.values()` would return an array containing all the values from the key-value pairs within the rooms map.
2. We loop over every room in the rooms map in a bid to find out what rooms the disconnecting client joined.
3. Using the socket ID as a reference, we iterate over the list of players in the current room being looped over to find out if the disconnecting client/player is part of it.
4. If the disconnecting client is part of the room, we simply emit a `playerDisconnected` event to the room, this event will be received by the other players in the room.

In order to notify the user when an opponent has been disconnected, the client-side application must listen for the `playerDisconnected` event.

Add a new `useEffect` hook in the `Game` component in `Game.js`, the listener will be registered in this hook as seen below:

```javascript
function Game({ players, username, room, orientation }) {
  // ...
  
  useEffect(() => {
    socket.on('playerDisconnected', (player) => {
      setOver(`${player.username} has disconnected`); // set game over
    });
  }, []);
  
  // ...
}
```

When the `playerDiconnected` event is received, we simply set the over state to "(username) has disconnected". This state change causes the "game over" alert dialog to pop up as shown below.

![-](https://blog.openreplay.com/images/building-a-chess-game-with-react/images/5uHecDr.gif)

## Freeing Up Resources
After a game is finished, it is necessary for both the client and server to release the resources that were used to initialize the game. This involves closing the game room and removing it from the rooms map on the backend, as well as having all client sockets exit the room.
 
To implement this, a new event called `closeRoom` needs to be created. This event will be triggered when a player clicks on the "Continue" button in the "game over" alert dialog. To set up this event, the `handleContinue` function prop of the "game over" `CustomDialog` element in `Game.js` should be updated to emit a "closeRoom" event on the websocket connection:
 
`Game.js`
```javascript
function Game({ players, room, orientation, cleanup }) {
  // ...
 
  return (
    // ...
      <CustomDialog // Game Over CustomDialog
        open={Boolean(over)}
        title={over}
        contentText={over}
        handleContinue={() => {
          socket.emit("closeRoom", { roomId: room });
          cleanup();
        }}
      />
    // ...
  );
}
```
In the code snippet above, we only modified the `handleContinue` function prop of the `CustomDialog` component that appears when a game is finished. In the `handleContinue` function, a `closeRoom` event is transmitted to the server, along with the room ID as data. The backend will manage the room closure, as you will soon discover. Following that, we invoke the `cleanup` function, which resets the state of the application, as previously described.
        
The server has to listen to the `closeRoom` event and perform the operations to close a room and free up resources. Let's add a new `closeRoom` event listener in the `io.on("connection")` callback function in the `server.js` file:
        
```javascript
io.on("connection", (socket) => {
  // ...

  socket.on("closeRoom", async (data) => {
    socket.to(data.roomId).emit("closeRoom", data); // <- 1 inform others in the room that the room is closing

    const clientSockets = await io.in(data.roomId).fetchSockets(); // <- 2 get all sockets in a room

    // loop over each socket client
    clientSockets.forEach((s) => {
      s.leave(data.roomId); // <- 3 and make them leave the room on socket.io
    });

    rooms.delete(data.roomId); // <- 4 delete room from rooms map
  });
});
```

Notice the line number comments in the code above; each numbered line below corresponds to a line-number comment in the code above.
 
1. The `closeRoom` event is broadcasted to the other clients in the room to let them know that the room is closing.
2. All clients that have joined a room (using the socket.join method) are retrieved. The value of the variable `fetchedSockets` is an array of all clients in the room.
3. The array of client sockets is iterated over, and each socket is made to leave the room using the `socket.leave` method.
4. Finally, the room is removed from the map of rooms.

As explained earlier, when a `closeRoom` event is received by the server, the server broadcasts the event to all clients in the room except the client that generated the event. The client applications need to listen to the `closeRoom` event in order to free up resources and leave the game environment. We can set up this listener in a new `useEffect` hook in the `Game` component. Add a new `useEffect` hook to the `Game` component in `Game.js` as shown below:

```javascript
// ...
function Game({ players, room, orientation, cleanup }) {
  // ...

  useEffect(() => {
    socket.on('closeRoom', ({ roomId }) => {
      if (roomId === room) {
        cleanup();
      }
    });
  }, [room, cleanup]);
  
  // ...
}
```
In the code snippet above, when the client application receives a `closeRoom` event, it first checks if the ID of the closed room is the same as the currently joined room. It then calls `cleanup`, which resets the application's state and exits the game environment. By resetting the `room` state, the `Game` component is unmounted and the `InitGame` component is mounted in its place.

To execute the code, run the application and create two browser tabs. Then, on one tab, create a game room, and on the other, join the room. Try to obtain a checkmate, which will trigger the "game over" alert dialog to appear. Once you click "Continue", the "closeRoom" event will be transmitted via the websocket connection, and the cleanup function will be executed after sending the event. As a result, the Game component will be unmounted. The other client in the second tab will receive the "closeRoom" event, causing it to leave the game environment. This is demonstrated in the following example:

![-](https://blog.openreplay.com/images/building-a-chess-game-with-react/images/98PiZHl.gif)

## Conclusion
In this article, we've learned how to build a multiplayer chess game using `react-chessboard` to render the chessboard, `chess.js` for chess logic including moves and validation, and `socket.io` for websocket connectivity.

If you're considering building a multiplayer chess game for production, here are some suggestions to make the app production-ready:

1. Implement an authentication system to attach persistent user IDs to client sockets. This way, a client's websocket connection can be easily identified. Here's an example that illustrates the concept.
2. Persist room data and game state. On the backend, store the game state and room data on a persistent and fast database like Redis. This way, if the server is restarted, room data and game states won't be lost. When a disconnected client app reconnects, the room data and game state can be retrieved from the backend, and the client app can continue the game.