Chidume Nnamdi I'm a software engineer with over six years of experience. I've worked with different stacks, including WAMP, MERN, and MEAN. My language of choice is JavaScript; frameworks are Angular and Node.js.

Build a Tic-Tac-Toe game with React Hooks

8 min read 2462

React Tic Tac Toe Hooks

Tic-Tac-Toe is one of the oldest and most popular board games in the world. We can play Tic-Tac-Toe with our friends physically using boards, pebbles, sticks, etc., but we can also play the game with our friends on a web browser. In this tutorial, we’ll build a Tic-Tac-Toe game from scratch using React Hooks, learning how to program a computer to play against us.

You can see the full code for the TicTacToe game at the GitHub repository. Let’s get started!

Table of contents

The logic of Tic-Tac-Toe

Tic-Tac-Toe is played on a 3×3 grid. The player who has the first move is X. The player who plays second is O. The first player to occupy three spaces in a row, column, or diagonal wins. There is no rule as to who goes first, however, a popular convention for deciding who starts is to roll a dice or flip a coin.

Tic-Tac-Toe gameplay can easily lead to a draw, in which case the players have to restart the game. In the next sections, we’ll learn how to implement the Tic-Tac-Toe game logic in the browser.

Installation

We’ll need the latest version of Node.js, Yarn, or npm installed on our machine. Keep in mind that npm comes bundled with Node.js, but if you’re using Yarn, you can install the latest version by running the command below:

npm i -g yarn

We can download the latest version of Next.js from https://nodejs.org/download.

Scaffold the project

We’ll use Yarn to scaffold our Next.js project. Run the command below from any folder on your computer:

yarn create next-app tictactoe
cd tictactoe

In the pages folder, we’ll create a folder called components, which will house our game. We’ll create a TicTacToe component in this folder as follows:

mkdir components

mkdir components/TicTacToe

touch components/TicTacToe/index.js components/TicTacToe/TicTacToe.module.css

components/TicTacToe/index.js contains the game and its logic, while components/TicTacToe/TicTacToe.module.css: contains the game styling.

Build the Tic-Tac-Toe game

Go into the styles/Home.module.css file and paste the following code:



.container {
  padding: 0 2rem;
}

.main {
  min-height: 100vh;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

Next, we’ll go into the pages/index.js and paste the following code:

import Head from "next/head";
import styles from "../styles/Home.module.css";
import TicTacToe from "../components/TicTacToe";

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Tic Tac Toe game</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
        <style>
          @import url('https://fonts.googleapis.com/css2?family=Indie+Flower');
        </style>
      </Head>

      <main className={styles.main}>
        <TicTacToe />
      </main>
    </div>
  );
}

The Home component will load on every route of our project. Notice that we imported our TicTacToe component and the stylesheet Home.module.css.

Inside the Home component, we set up a Head. Inside this Head, we have title and meta tags. We also have a style tag to load the font. We load the Indie Flower font to display the X and O on the board. Lastly, in the main tag, we render the TicTacToe component.

The TicTacToe component

Let’s move to the TicTacToe component. Our board will be a square of 3×3 cells, and players will compete inside the cells. To define our board, we’ll use a multi-dimensional array:

[
  ["", "", ""],
  ["", "", ""],
  ["", "", ""],
];

The first dimension is the row, and the second dimension is the column. The rows and columns are numbered from 0 to 2. The first row is 0, the second row is 1, and the third row is 2. The first column is 0, the second column is 1, and the third column is 2.

The first row is the top row, the second row is the middle row, and the third row is the bottom row. The first column is the left column, the second column is the middle column, and the third column is the right column.

We’ll draw the board on the DOM from this array. The first cell is in the first row and the first column, the second cell is in the first row and second column, and the third cell is in the first row and third column. It follows this pattern until the last cell.

First, we import the React library and the styles from the TicTacToe.module.css file:

// TickTacToe/index.js
import styles from "./TicTacToe.module.css";
import { useEffect, useState } from "react";

Next, we’ll create an object that will hold the name and symbol of each player in the game. For this tutorial, it’ll be us against the computer:


More great articles from LogRocket:


// TickTacToe/index.js
// previous code here

const players = {
  CPU: {
    SYM: "O",
    NAME: "CPU",
  },
  HUMAN: {
    SYM: "X",
    NAME: "You",
  },
};

The players object holds the name and symbol of the players. CPU is the name of the computer, and O is the symbol of the computer. You is the name of the human, and X is the symbol of the human.

Next, we’ll create a function sleep, which we’ll use to simulate computer thinking:

// TickTacToe/index.js
// previous code here

function sleep(milliseconds) {
  const date = Date.now();
  let currentDate = null;
  do {
    currentDate = Date.now();
  } while (currentDate - date < milliseconds);
}

The code above will wait for the specified number of milliseconds before releasing the CPU.
Now, we create the TicTacToe component:

// TickTacToe/TicTacToe.js
//  previous code here

export default function TicTacToe() {}

To hold the board, we create a useState Hook. We’ll create additional Hooks to hold the turn and the winner:

// TickTacToe/TicTacToe.js
// previous code here
export default function TicTacToe() {
  const [board, setBoard] = useState([
    ["", "", ""],
    ["", "", ""],
    ["", "", ""],
  ]);
  const [isCPUNext, setIsCPUNext] = useState(false);
  const [winner, setWinner] = useState(null);
}
  • board state: Holds the multi-dimensional array of the board
  • setBoard function: Sets the board
  • isCPUNext state: Holds the turn of the player
  • setIsCPUNext function: Sets the turn
  • winner state: Holds the winner of the game
  • setWinner function: Sets the winner

Next, we render the UI as follows:

// TickTacToe/TicTacToe.js
// previous code here
export default function TicTacToe() {
  // code here

  return (
    <div className={styles.container}>
      <div className={styles.col}>
        <span onClick={() => playFn(0, 0)} className={styles.cell}>
          {board\[0\][0]}
        </span>
        <span onClick={() => playFn(0, 1)} className={styles.cell}>
          {board\[0\][1]}
        </span>
        <span onClick={() => playFn(0, 2)} className={styles.cell}>
          {board\[0\][2]}
        </span>
      </div>
      <div className={styles.col}>
        <span onClick={() => playFn(1, 0)} className={styles.cell}>
          {board\[1\][0]}
        </span>
        <span onClick={() => playFn(1, 1)} className={styles.cell}>
          {board\[1\][1]}
        </span>
        <span onClick={() => playFn(1, 2)} className={styles.cell}>
          {board\[1\][2]}
        </span>
      </div>
      <div className={styles.col}>
        <span onClick={() => playFn(2, 0)} className={styles.cell}>
          {board\[2\][0]}
        </span>
        <span onClick={() => playFn(2, 1)} className={styles.cell}>
          {board\[2\][1]}
        </span>
        <span onClick={() => playFn(2, 2)} className={styles.cell}>
          {board\[2\][2]}
        </span>
      </div>
    </div>
  );
}

In the code above, we have a container that encloses each cell and the cols. Each col is a row of the board. Each cell is a cell of the board. When a cell is clicked, the playFn function will be called.

See that in the first cell, we displayed the value, thereby getting the first array in the board array and the first element in the array. Likewise, we did the same for the second cell and eventually the rest of the cells.

We also called the playFn function with the appropriate index of the array in the board and the index in that same array, enabling us to capture wherein the board we will place the symbol.

However, we’re missing a few things. For example, we need to indicate whose turn it is, display the winner of the game, and add a button to play again when the game is over:

// TickTacToe/TicTacToe.js
// previous code here
export default function TicTacToe() {
  // code here
  return;
  <div>
    <div>{!winner && displayTurn()}</div>
    {/** previous JSX code here **/}
    {winner && <h2>{displayWinner()}</h2>}
    {winner && (
      <button className={styles.video_game_button} onClick={playAgainFn}>
        Play Again
      </button>
    )}
  </div>;
}

We’ll create the function playFn. When a user clicks on it, it will play the game:

function playFn(arrayIndex, index) {
  if (isCPUNext) return;
  if (winner) return;
  board\[arrayIndex\][index] = players?.HUMAN?.SYM;
  setBoard((board) => [...board]);
  checkWinner();
  setIsCPUNext(true);
}

The playFn function will check if the game is over. If it is, it will display the winner. If not, it will check if the player is next. If it is, it will set the player’s symbol to the particular index of the array in the board array and call the setBoard function to set the board state. Then, it will call the checkWinner function to check if the game is over. Finally, it will set isCPUNext to true.

Next, we’ll set up a useEffect Hook:

useEffect(() => {
  if (winner) return;
  if (isCPUNext) {
    cPUPlay();
  }
}, [isCPUNext]);

This useEffect Hook runs once the component is mounted, checking if the game is over. If it is, it will return. If it is not, it will check if the CPU is next. If it is, it will call the cPUPlay function to play the game.

Next, we’ll create the cPUPlay function:

function cPUPlay() {
  if (winner) return;
  sleep(1000);

  const cPUMove = getCPUTurn();

  board\[cPUMove.arrayIndex\][cPUMove.index] = players?.CPU?.SYM;

  setBoard((board) => [...board]);
  checkWinner();
  setIsCPUNext(false);
}

The cPUPlay function checks if the game is over. If it is, it will return. If it is not, it will call sleep for 10 seconds, then call the getCPUTurn function to get the CPU’s move. It will set the CPU’s symbol to the particular index of the array in the board array and call the setBoard function to set the board state. Then, it will call the checkWinner function to check if the game is over. Finally, it will set the isCPUNext to false.

Next, we’ll create the getCPUTurn function, which will return the CPU’s move:

function getCPUTurn() {
  const emptyIndexes = [];
  board.forEach((row, arrayIndex) => {
    row.forEach((cell, index) => {
      if (cell === "") {
        emptyIndexes.push({ arrayIndex, index });
      }
    });
  });
  const randomIndex = Math.floor(Math.random() * emptyIndexes.length);
  return emptyIndexes[randomIndex];
}

This function will loop through the board array and find all the empty cells, then, it will randomly select one of the empty cells.

Next, we’ll create the checkWinner function, which will check if the game is over. If it is, it will return. If it is not, it will check if the player or the CPU has won. If the player has won, it will set the winner state to HUMAN. If the CPU has won, it will set the winner state to CPU. If it is a draw, it will set the winner state to draw. If it is not a draw, it will return:

function checkWinner() {
  // check same row
  for (let index = 0; index < board.length; index++) {
    const row = board[index];
    if (row.every((cell) => cell === players?.CPU?.SYM)) {
      setWinner(players?.CPU?.NAME);
      return;
    } else if (row.every((cell) => cell === players?.HUMAN?.SYM)) {
      setWinner(players?.HUMAN?.NAME);
      return;
    }
  }

  // check same column
  for (let i = 0; i < 3; i++) {
    const column = board.map((row) => row[i]);
    if (column.every((cell) => cell === players?.CPU?.SYM)) {
      setWinner(players?.CPU?.NAME);
      return;
    } else if (column.every((cell) => cell === players?.HUMAN?.SYM)) {
      setWinner(players?.HUMAN?.NAME);
      return;
    }
  }

  // check same diagonal
  const diagonal1 = \[board[0\][0], board\[1\][1], board\[2\][2]];
  const diagonal2 = \[board[0\][2], board\[1\][1], board\[2\][0]];
  if (diagonal1.every((cell) => cell === players?.CPU?.SYM)) {
    setWinner(players?.CPU?.NAME);
    return;
  } else if (diagonal1.every((cell) => cell === players?.HUMAN?.SYM)) {
    setWinner(players?.HUMAN?.NAME);
    return;
  } else if (diagonal2.every((cell) => cell === players?.CPU?.SYM)) {
    setWinner(players?.CPU?.NAME);
    return;
  } else if (diagonal2.every((cell) => cell === players?.HUMAN?.SYM)) {
    setWinner(players?.HUMAN?.NAME);
    return;
  } else if (board.flat().every((cell) => cell !== "")) {
    setWinner("draw");
    return;
  } else {
    setWinner(null);
    return;
  }
}

The code is quite simple. It loops through the board array and checks if the same row, column, or diagonal has the same symbol. If it does, it will set the winner state to the player’s name. The game is a draw if all the boards are filled with no matching symbols found. It will then set the winner state to draw.

Let’s create the displayWinner function:

function displayWinner() {
  if (winner === "draw") {
    return "It's a draw!";
  } else if (winner) {
    return `${winner} won!`;
  }
}

If the winner state is draw, it will return It's a draw!. If the winner state is not draw, it will return ${winner} won!. Now, we’ll create the displayTurn function:

function displayTurn() {
  if (isCPUNext) {
    return "CPU's turn";
  } else {
    return "Your turn";
  }
}

The displayTurn function will check if the CPU is next. If it is, it will return CPU's turn. If it is not, it will return Your turn.

We’ll create the playAgainFn function:

function playAgainFn() {
  setBoard([
    ["", "", ""],
    ["", "", ""],
    ["", "", ""],
  ]);
  setWinner(null);
  setIsCPUNext(false);
}

This function will set the board state to the initial board state and set the winner state to null. Then, it will set the isCPUNext state to false.

Styling the app

Open the TicTacToe.module.css file and add the following styles:

.container {
  display: flex;
  flex-direction: column;
  background-image: url(https://i.imgur.com/OVBsgc1.jpg);
  border: 2px solid black;
  font-family: "Indie Flower", cursive;
}

.col {
  display: flex;
}

.cell {
  width: 89px;
  height: 89px;
  border: 1px solid lightgray;
  text-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: xxx-large;
  text-transform: capitalize;
  cursor: pointer;
  color: white;
  font-weight: 900;
}

.video_game_button {
  text-shadow: 1px 1px pink, -1px -1px maroon;

  line-height: 1.5em;
  text-align: center;
  display: inline-block;
  width: 4.5em;
  height: 4.5em;
  border-radius: 50%;
  background-color: red;
  box-shadow: 0 0.2em maroon;
  color: red;
  margin: 5px;
  background-color: red;
  background-image: linear-gradient(left top, pink 3%, red 22%, maroon 99%);
  cursor: pointer;
  padding-left: 5px;
}

.video_game_button:active {
  box-shadow: none;
  position: relative;
  top: 0.2em;
}

Play the game

Tic Tac Toe React Hooks

Conclusion

In this tutorial, we explained how Tic-Tac-Toe is played. Then, we scaffolded a Next.js project, going on to show how we can code and play a Tic-Tac-Toe game on our computer.

Our computer gameplay has minimal or arcane logic. It would be nice to spice up the logic so the computer can think and play like a human being. We can also make the game more fun by adding some fun features like a chat, a leaderboard, and a game history.

I can’t wait to see what you come up with! Happy coding!

 

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Chidume Nnamdi I'm a software engineer with over six years of experience. I've worked with different stacks, including WAMP, MERN, and MEAN. My language of choice is JavaScript; frameworks are Angular and Node.js.

Leave a Reply