Building Tic Tac Toe with React using Typescript, Hooks, and Ant Design

Building Tic Tac Toe with React using Typescript, Hooks, and Ant Design

We all have played this game in our life and it’s so much fun. I always wanted to build a Tic Tac Toe game ever since I saw and played.

Also, I wanted to explore React with Typescript. So, I realized that It will be a great idea to build Tic Tac Toe in React using Typescript. It will give me the exposure to Typescript that I need and at the same time It will be fun to build this game.

We will use React, Typescript, Hooks, and Ant Design for this. If you don’t have any idea of Typescript. I would highly recommend getting the basic idea.

So, Take a cup of tea and we are going to build a Tic Tac Toe in React using Typescript.

Create an App and set up a basic layout

We will create a React App using create-react-app

npx create-react-app tic-tac-toe --template typescript
cd tic-tac-toe
npm start

Install Ant Design

npm install antd

Now, you have created an app. Let’s fix the layout.
Remove all unnecessary files from the layout and make it as given below.

A couple of things:

  1. Let’s start by creating a components folder.

  2. Don’t worry about what will be inside the common and Utilities folder. You will get to know as we proceed.

  3. Create files such Start.tsx Finish.tsx HumanvsHuman.tsx HumanvsComputer.tsx .

  4. In App.css remove everything except .App class, maybe we need that in the future but make that class also empty.

Let’s fix index.css

Just copy and paste the below content of index.css into your file.
You can download the background image from here.

body {
  margin: 0;
  /* background-color: #44c4eb; */
  background-image: url('./components/common/tic-tac-toe-seamless-background-on-dark-blue-illustration-vector.jpg');
  background-repeat: no-repeat;
  background-attachment: fixed;
  background-size: cover;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

Start Page

We will first build the Start Page. Firstly, create a file name Start.tsx in components folder. Now, the Start page will have a couple of options to select from.

  1. Letters to choose i.e. X, O.

  2. Number of Players to select i.e. Player 1 means with Computer and Player 2 means with another Player.

The start component will accept props named:

  • gameStatus — It shows the status of the game i.e. “started”, “playing”, and “finished”. By Default: “started”.

  • player — It stores which opponent is chosen by the user i.e. “human” or “computer”.

  • letter — It stores the letter selected by the user i.e. “X” or “O”.

We are using the interface here to declare what props the Start component will accept. Once the user selects all the options and presses the Start Game!! button then we are changing the status of the game to “playing”.

import React from "react";
import type { RadioChangeEvent } from "antd";
import { Radio, Typography, Button, Space } from "antd";

const { Title } = Typography;

interface Props {
  gameStatus: string;
  setGameStatus: (val: string) => void;
  player: string;
  setPlayer: (val: string) => void;
  letter: string;
  setLetter: (val: string) => void;
}

const styles = {
  letterButton: {
    fontWeight: "bold",
    fontSize: "30px",
    width: "150px",
    color: "red",
  },
  playerButton: {
    fontWeight: "bold",
    fontSize: "25px",
    width: "200px",
    color: "red",
  },
  startButton: {
    marginTop: "15px",
    fontWeight: "bold",
    width: "300px",
  },
};

const Start: React.FC<Props> = ({
  gameStatus,
  setGameStatus,
  player,
  setPlayer,
  letter,
  setLetter,
}) => {
  const handleLetterChange = (event: RadioChangeEvent): void => {
    // console.log("l: ", event.target.value);
    setLetter(event.target.value);
  };

  const handlePlayerChange = (event: RadioChangeEvent): void => {
    // console.log("p: ", event.target.value);
    setPlayer(event.target.value);
  };

  const handleStartGame = () => {
    setGameStatus("playing");
  };

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        justifyContent: "center",
        alignItems: "center",
        textAlign: "center",
      }}
    >
      <Title style={{ color: "#ebc334" }}>Choose your Letter</Title>

      <Radio.Group
        buttonStyle="solid"
        size="large"
        onChange={handleLetterChange}
      >
        <Space direction="vertical">
          <Radio.Button style={styles.letterButton} value="X">
            X
          </Radio.Button>
          <Radio.Button style={styles.letterButton} value="O">
            O
          </Radio.Button>
        </Space>
      </Radio.Group>

      {letter && (
        <Title style={{ color: "#ebc334" }}>
          Whom do you want to play with?
        </Title>
      )}

      {letter && (
        <Radio.Group buttonStyle="solid" onChange={handlePlayerChange}>
          <Space direction="vertical">
            <Radio.Button style={styles.playerButton} value="computer">
              1 Player
            </Radio.Button>
            <Radio.Button style={styles.playerButton} value="human">
              2 Player
            </Radio.Button>
          </Space>
        </Radio.Group>
      )}

      {letter && player && (
        <Button
          style={styles.startButton}
          type="primary"
          danger
          size="large"
          onClick={handleStartGame}
        >
          Start Game!!
        </Button>
      )}
    </div>
  );
};

export default Start;

Building the Utility

We all know that the Tic Tac Toe board is one of the most important parts of the game. And that board consists of 9 squares. Let’s build a utility called Square which is going to be used to design the board.

Create a file name Square.tsx in components/Utilities folder. Square.tsx accepts props named value, index, handleClick function.

A specific square holds information such as index, value, and onClick function. This means I want to know, for any square whenever the onClick function triggers, I should have what index that square has and what value it contains.

import React from "react";
import { Button } from "antd";

interface Props {
  value: string;
  index: number;
  handleClick: (index: number) => void;
}

const styles = {
  square: {
    height: "120px",
    width: "120px",
    backgroundColor: "#eb8034",
    fontSize: "50px",
    color: "red",
    fontWeight: "bold",
  },
};

const Square: React.FC<Props> = ({ value, index, handleClick }) => {
  return (
    <Button style={styles.square} onClick={() => handleClick(index)}>
      {value}
    </Button>
  );
};

export default Square;

Building the game

This is the most interesting part because we will be working on the core of tic tac toe. There could be two scenarios I can think of.

  1. Playing with Computer (1 Player)

  2. Playing with Human (2 Player)

Before we start working on these two cases. Let’s build a common function name findWinner . We need this because we will have to find the winner in both cases.

Create a file name findWinner.ts in components/common the folder.

Few things about findWinner function:

  • There will be a total of 8 combinations to win the game.

  • You will have to keep on running the loop until you get a winner.

  • You will have to check for all 8 combinations to find the winner.

  • This function will return getWinner , a variable that has either “X” or “O” as a winner. If it contains “” i.e. empty string that means the game is “Tied”.

export const findWinner = (board: string[]): string => {
  const winningPositions = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];

  let winningPositionIndex = 0;
  let getWinner = "";

  while (winningPositionIndex < winningPositions.length && getWinner === "") {
    // Get the combination to check
    const combinationToCheck = winningPositions[winningPositionIndex];

    // Get the value to check. Also, you can't check empty string cuz it might get failed.
    // You have to check with either X or O
    const valuesToCheck = combinationToCheck.map((el) => board[el]);

    // Check for every value
    const isFinished = valuesToCheck.every(
      (el) => el === valuesToCheck[0] && valuesToCheck[0]
    );

    getWinner = isFinished ? valuesToCheck[0] : "";

    winningPositionIndex++;
  }
  return getWinner;
};

Building HumanvsHuman aka (2 Player)

Create a file name HumanvsHuman.tsx in the components folder.

Some points about HumanvsHuman.tsx component:

  • It accepts props such as gameStatus which just store the status of the game as we have discussed already.

  • You might be thinking why it doesn’t take letter prop as we might need this. Well, in this case, it doesn’t matter.

  • The game will start with “X” by default in this case.

  • In handleClick function, we are setting the value on board as per the turn on the respective index.

  • One thing to remember is, once the user has clicked on the square and it got value then the value shouldn’t be changed no matter how many times the user clicks on that specific square again.

  • Every time board gets updated, we need to find a winner. Thus we need to run the useEffect whenever the boards get updated and it will call the findWinner function.

  • Once the game is finished, store the winner in the local storage and change the status of the game to finished .

import React, { useState, useEffect } from "react";
import { Typography } from "antd";

import Square from "./Utilities/Square";
import { findWinner } from "./common/findWinner";

interface Props {
  gameStatus: string;
  setGameStatus: (val: string) => void;
}

const { Title } = Typography;

const styles = {
  board: {
    display: "grid",
    gridTemplateColumns: "repeat(3, 1fr)",
  },
};

const HumanvsHuman: React.FC<Props> = ({ gameStatus, setGameStatus }) => {
  const [board, setBoard] = useState<string[]>(Array(9).fill(""));
  const [turn, setTurn] = useState<string>("X");

  useEffect(() => {
    const getWinner = findWinner(board);

    if (getWinner) {
      localStorage.setItem("winner", getWinner);
      setGameStatus("finished");
      return;
    } else {
      const data = board.every((v) => v !== "");

      if (data) {
        localStorage.setItem("winner", getWinner);
        setGameStatus("finished");
      }
      return;
    }
  }, [board, setGameStatus]);

  const handleClick = (index: number): void => {
    // console.log("index", index);

    if (index < 0 || index > 9 || board[index] !== "") return;

    const newBoard = [...board];

    newBoard[index] = turn;
    setBoard(newBoard);

    const newTurn = turn === "X" ? "O" : "X";
    setTurn(newTurn);
  };

  return (
    <div>
      {turn.length > 0 && (
        <Title
          level={2}
          style={{ textAlign: "center", marginTop: "20px", color: "#ebc334" }}
        >
          Player {turn} Turn
        </Title>
      )}
      <div
        style={{
          display: "flex",
          justifyContent: "center",
          marginTop: "20px",
        }}
      >
        {/* -----------------BUILDING THE BOARD-------------- */}
        <div style={styles.board}>
          {board.map((value, index) => {
            return (
              <Square
                key={index}
                value={value}
                index={index}
                handleClick={handleClick}
              />
            );
          })}
        </div>
      </div>
    </div>
  );
};

export default HumanvsHuman;

Building HumanvsComputer aka (1 Player)

Create a file name HumanvsComputer.tsx in the components folder.

Some points about HumanvsComputer.tsx component:

  • It accepts props such as gameStatus and letter .

  • Now, you might think why we are getting the letter selected by the player as a prop. We haven’t done the same in the previous component.

  • Well, it’s necessary because if you select a letter from “X” or “O” the opposite one should automatically for the computer. So, we need that information.

  • Apart from this, handleClick function is pretty much the same. The change will only occur on square through this when the player has a turn.

  • By default, the player has to start the game in this case.

  • There is one more thing to notice, how do we decide which one has a turn now? Well, you can see in the code that we are having a state variable name turn . It has a letter value by default. So, the player will have to start the game in this case.

  • When turn equals the letter selected by the player which means we have a player turn, and if the turn doesn’t equals a letter then we have a computer turn.

  • In this case, the computer will place only in empty positions. Thus, we will randomly select the empty position for the computer turn.

  • Every time the board gets updated, we have to run the useEffect . This will do two things. One, the computer will automatically play its turn whenever it gets, and second, we will have to call the findWinner function to get the winner.

  • Once the game is finished, store the winner in the local storage and change the status of the game to finished .

import React, { useState, useEffect } from "react";

import Square from "./Utilities/Square";
import { findWinner } from "./common/findWinner";

interface Props {
  gameStatus: string;
  setGameStatus: (val: string) => void;
  letter: string;
}

const styles = {
  board: {
    display: "grid",
    gridTemplateColumns: "repeat(3, 1fr)",
  },
};

const HumanvsComputer: React.FC<Props> = ({
  gameStatus,
  setGameStatus,
  letter,
}) => {
  const [board, setBoard] = useState<string[]>(Array(9).fill(""));
  const [turn, setTurn] = useState<string>(letter);

  useEffect(() => {
    // Computer turn
    if (letter !== turn) {
      const emptyIndexes = board
        .map((el, index) => (el === "" ? index : ""))
        .filter((val) => val !== "");

      const randomIndex =
        emptyIndexes[Math.floor(Math.random() * emptyIndexes.length)];

      const newBoard = board;
      newBoard[
        typeof randomIndex === "number"
          ? randomIndex
          : parseInt(randomIndex, 10)
      ] = turn;
      setBoard(newBoard);

      const newTurn = turn === "X" ? "O" : "X";
      setTurn(newTurn);
    }
    const getWinner = findWinner(board);
    if (getWinner) {
      localStorage.setItem("winner", getWinner);
      setGameStatus("finished");
      return;
    } else {
      const data = board.every((v) => v !== "");

      if (data) {
        localStorage.setItem("winner", getWinner);
        setGameStatus("finished");
      }
      return;
    }
  }, [board, turn, setGameStatus, letter]);

  const handleClick = (index: number): void => {
    // console.log("index", index);

    if (index < 0 || index > 9 || board[index] !== "") return;

    if (letter === turn) {
      const newBoard = [...board];

      newBoard[index] = turn;
      setBoard(newBoard);

      const newTurn = turn === "X" ? "O" : "X";
      setTurn(newTurn);
    }
  };

  return (
    <div>
      {/* -----------------BUILDING THE BOARD-------------- */}
      <div
        style={{
          display: "flex",
          justifyContent: "center",
          marginTop: "20px",
        }}
      >
        <div style={styles.board}>
          {board.map((value, index) => {
            return (
              <Square
                key={index}
                value={value}
                index={index}
                handleClick={handleClick}
              />
            );
          })}
        </div>
      </div>
    </div>
  );
};

export default HumanvsComputer;

Now, you might question whether I could’ve made HumanvsHuman and HumanvsComputer in a single file. You are correct, but I wanted to handle both scenarios in a different file. So, It will be easy for me to deal with bugs in the future.

Finish Page

Create a file name Finish.tsx in the components folder.

This component does two things. One, declare the winner, and second, reset all the states.

A couple of things about the Finish component:

  • It accepts all the props such as gameStatus , letter and player .

  • There is a restart button that resets the game to start over again.

  • We get the winner value from the local storage to show it on the page.

import React from "react";
import { Button, Typography } from "antd";

const { Title } = Typography;

interface Props {
  gameStatus: string;
  setGameStatus: (val: string) => void;
  player: string;
  setPlayer: (val: string) => void;
  letter: string;
  setLetter: (val: string) => void;
}

const styles = {
  resetButton: {
    fontWeight: "bold",
  },
};

const Finish: React.FC<Props> = ({
  gameStatus,
  setGameStatus,
  setPlayer,
  player,
  letter,
  setLetter,
}) => {
  const winner = localStorage.getItem("winner");

  const handleRestart = () => {
    localStorage.removeItem("winner");
    setGameStatus("started");
    setPlayer("");
    setLetter("");
  };

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        justifyContent: "center",
        alignItems: "center",
        textAlign: "center",
      }}
    >
      {winner === "" ? (
        <>
          <Title style={{ color: "#ebc334" }}>😅 Oops!</Title>
          <Title style={{ color: "#ebc334" }}>Game is Tied 😟</Title>
        </>
      ) : (
        <>
          <Title style={{ color: "#ebc334" }}>
            🎉 🎊 YAY, We have a winner
          </Title>
          {player === "human" ? (
            <Title style={{ color: "#ebc334" }} level={2}>
              Player {winner} won the game
            </Title>
          ) : letter === winner ? (
            <Title style={{ color: "#ebc334" }} level={2}>
              You won the game
            </Title>
          ) : (
            <Title style={{ color: "#ebc334" }} level={2}>
              Computer won the game
            </Title>
          )}
        </>
      )}
      <Button
        style={styles.resetButton}
        size="large"
        type="primary"
        danger
        onClick={() => handleRestart()}
      >
        Restart the Game!!
      </Button>
    </div>
  );
};

export default Finish;

I hope you’ve enjoyed this tutorial. This was a lot of fun to make. If you feel stuck on something, please refer to the source code and If you have any questions, let me know in the comments.

Thanks!