DEV Community

Cover image for Building a Tic Tac Toe App with React.js: A Step-by-Step Guide 2024
Tyler Duncan Sotubo
Tyler Duncan Sotubo

Posted on

Building a Tic Tac Toe App with React.js: A Step-by-Step Guide 2024

Introduction:

Welcome to our step-by-step guide on creating a Tic Tac Toe game using React.js! React.js is a powerful JavaScript library for building user interfaces, and it's an excellent choice for creating interactive and dynamic web applications. In this tutorial, we'll walk you through the process of building a simple Tic Tac Toe game to help you understand the fundamentals of React.js.

Prerequisites:

To follow this tutorial, you will need the following:

  • Node.js version 12.2.0 or higher is installed on your machine. You can install the latest version of Node.js here Node.js.
  • Familiarity with HTML, CSS, and modern JavaScript. It also helps to know the modern JS used in React.
  • A foundational knowledge of React.

Step 1 — Creating a Vite Project

In this step, you will create a new React project using the Vite tool from the command line. You will use the npm package manager to install and run the scripts.

Run the following command in your terminal to scaffold a new Vite project:

npm create vite@latest
Enter fullscreen mode Exit fullscreen mode

After the script finishes, you will be prompted to enter a project name: Type in tic-tac-toe

Project name:tic-tac-toe
Enter fullscreen mode Exit fullscreen mode

After entering your project name, Vite will prompt you to select a framework:

Output
Select a framework: » - Use arrow keys. Return to submit.
Vanilla
Vue
> React
Preact
Lit
Svelte
Others
Enter fullscreen mode Exit fullscreen mode

Vite allows you to bootstrap a range of project types, not just React. Currently, it supports React, Preact, Vue, Lit, Svelte, and vanilla JavaScript projects.

Use your keyboard arrow key to select React.

After selecting the React framework, Vite will prompt you to choose the language type. You can use JavaScript or TypeScript to work on your project.

Use your arrow keys to select JavaScript:

Select a variant: » - Use arrow keys. Return to submit.
> JavaScript
TypeScript
JavaScript + SWC
TypeScript + SWC
Enter fullscreen mode Exit fullscreen mode

After setting up the framework, you will see an output that the project has been scaffolded. Vite will then instruct you to install dependencies using npm:

Done. Now run:
 cd tic-tac-toe
 npm install
 npm run dev
Enter fullscreen mode Exit fullscreen mode

Navigate to your project folder as directed:

$ cd tic-tac-toe
Enter fullscreen mode Exit fullscreen mode

Then install npm on the project:

 npm install
Enter fullscreen mode Exit fullscreen mode

Step 2 — Starting the Development Server

 npm run dev
Enter fullscreen mode Exit fullscreen mode

It would run your project in development mode and get you stared

> tic-tac-toe@0.0.0 dev
> vite

  VITE v5.0.12  ready in 310 ms
    Local:   http://localhost:5173/
    Network: use --host to expose
    press h + enter to show help
Enter fullscreen mode Exit fullscreen mode

Next, open your browser and visit http://localhost:5173/. The default React project will be running on port 5173:

Vite Boilerplate app.jsx page

Step 3 — Create TicTactoe, Board and Square Components

In the "src" folder, create a new file named "TicTactoe.jsx." This component will represent the Tic Tac Toe game board and hold all the logic of the game. Define the initial structure of the Tic Tac Toe and render it using React.

// TicTacToe.jsx

const TicTacToe = () => {
  return (
    <div>
     TicTacToe
    </div>
  );
};
export default TicTacToe;
Enter fullscreen mode Exit fullscreen mode

In the "src" folder, create a new file named "Board.jsx."This would all the squares and the UI structure of the game
// Board.jsx

const Board = () => {
  return (
    <div>
      Board
    </div>
  );
};
export default Board;
Enter fullscreen mode Exit fullscreen mode

In the "src" folder, create a new file named "Square.jsx." This component would represent every individual square in the game board

// Square.jsx

const Square = () => {
  return (
    <div>
    Square
    </div>
  );
};
export default Square;
Enter fullscreen mode Exit fullscreen mode

Step 4 — Connect all the components and create an array to track value and pass down as props to square component with value

import board.jsx into TicTacToe.jsx

// TicTacToe.jsx


import Board from "./Board";
const TicTacToe = () => {
  return (
    <div>
     <Board />
    </div>
  );
};
export default TicTacToe;
Enter fullscreen mode Exit fullscreen mode

import Square.jsx into Board.jsx

// Board.jsx

import Square from "./Square";
const Board = () => {
  return (
    <div>
     <Square />
    </div>
  );
};
export default Board;
Enter fullscreen mode Exit fullscreen mode

// TicTacToe.jsx
We would create an array and track the state with useState. import useState from react and create an array with the initial value of null to hold all the values of 9 squares

import { useState } from "react";
const [square, setSquare] = useState(Array(9).fill(null));
Enter fullscreen mode Exit fullscreen mode

Pass down square as a prop to Board component

import Board from "./Board";
import { useState } from "react";

const TicTacToe = () => {
const [square, setSquare] = useState(Array(9).fill(null));
  return (
    <div>
     <Board square={square} />
    </div>
  );
};
export default TicTacToe;
Enter fullscreen mode Exit fullscreen mode

Create a grid of 9 squares with Tailwind CSS and pass down the square state as custom values from index 0 - 8. Remember JavaScript arrays are zero-indexed

// Board.jsx

import Square from "./Square";
const Board = ({square}) => {
  return (
   <div className="grid grid-cols-3 grid-rows-3 relative">
    <Square value={square[0]} />
    <Square value={square[1]}/>
    <Square value={square[2]}/>
    <Square value={square[3]}/>
    <Square value={square[4]}/>
    <Square value={square[5]}/>
    <Square value={square[6]}/>
    <Square value={square[7]}/>
    <Square value={square[8]}/>
   </div>
  );
};
export default Board;
Enter fullscreen mode Exit fullscreen mode

In the square component use value as the content of each square
// Square.jsx

const Square = ({value}) => {
  return (
    <div>
      {value}
    </div>
  );
};
export default Square;
Enter fullscreen mode Exit fullscreen mode

Step 5 — Track each player Turn with useState and value X and O. Create a click event to change the state of each square on player click

import Board from "./Board";
import { useState } from "react";

const TicTacToe = () => {
const [square, setSquare] = useState(Array(9).fill(null));
const [player, setPlayer] = useState("X");

const handleSquareClick = (index) => {
   // Return if square already contents a value
    if (square[index] !== null) return;
   // Create a new array to add each square index 
    const newSquare = [...square];
    newSquare[index] = player;
   // set square array to the new Array
    setSquare(newSquare);
   // switch between X and O based on player turn
    setPlayer(player === "X" ? "O" : "X");
  };

  return (
    <div>
     <Board square={square} />
    </div>
  );
};
export default TicTacToe;
Enter fullscreen mode Exit fullscreen mode

Pass down the handleSquareClick to Board component

import Board from "./Board";
import { useState } from "react";

const TicTacToe = () => {
const [square, setSquare] = useState(Array(9).fill(null));
const [player, setPlayer] = useState("X");

const handleSquareClick = (index) => {
   // Return if square already contents a value
    if (square[index] !== null) return;
   // Create a new array to add each square index 
    const newSquare = [...square];
    newSquare[index] = player;
   // set square array to the new Array
    setSquare(newSquare);
   // switch between X and O based on player turn
    setPlayer(player === "X" ? "O" : "X");
  };

  return (
    <div>
     <Board square={square} squareClick={handleSquareClick} />
    </div>
  );
};
export default TicTacToe;
Enter fullscreen mode Exit fullscreen mode

Pass down the squareClick function to square component

// Board.jsx

import Square from "./Square";
const Board = ({square, squareClick}) => {
  return (
   <div className="grid grid-cols-3 grid-rows-3 relative">
    <Square value={square[0]} onClick={() => squareClick(0)}/>
    <Square value={square[1]} onClick={() => squareClick(1)}/>
    <Square value={square[2]} onClick={() => squareClick(2)}/>
    <Square value={square[3]} onClick={() => squareClick(3)}/>
    <Square value={square[4]} onClick={() => squareClick(4)}/>
    <Square value={square[5]} onClick={() => squareClick(5)}/>
    <Square value={square[6]} onClick={() => squareClick(6)}/>
    <Square value={square[7]} onClick={() => squareClick(7)}/>
    <Square value={square[8]} onClick={() => squareClick(8)}/>
   </div>
  );
};
export default Board;
Enter fullscreen mode Exit fullscreen mode

// Square.jsx

const Square = ({value,onClick}) => {
  return (
    <div onClick={onClick}>
      {value}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Step 6 — Create a Strike div in Board to update the board when we have a winning pattern

// Board.jsx

import Square from "./Square";
const Board = ({square, squareClick}) => {
  return (
<div className="flex flex-col">
   <div className="grid grid-cols-3 grid-rows-3 relative">
    <Square value={square[0]} onClick={() => squareClick(0)}/>
    <Square value={square[1]} onClick={() => squareClick(1)}/>
    <Square value={square[2]} onClick={() => squareClick(2)}/>
    <Square value={square[3]} onClick={() => squareClick(3)}/>
    <Square value={square[4]} onClick={() => squareClick(4)}/>
    <Square value={square[5]} onClick={() => squareClick(5)}/>
    <Square value={square[6]} onClick={() => squareClick(6)}/>
    <Square value={square[7]} onClick={() => squareClick(7)}/>
    <Square value={square[8]} onClick={() => squareClick(8)}/>
   </div>
   <div className={`absolute w-full bg-orange-600 z-40`}></div>
  </div>
  );
};
export default Board;
Enter fullscreen mode Exit fullscreen mode

strike

Step 6 — Styling the strike div and hover effect on each sqaure

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-size: 10px;
}
body {
  background-color: #f5f5f5;
}

.strike-row-1 {
  width: 100%;
  height: 4px;
  top: 15%;
}
.strike-row-2 {
  width: 100%;
  height: 4px;
  top: 48%;
}
.strike-row-3 {
  width: 100%;
  height: 4px;
  top: 83%;
}
.strike-column-1 {
  height: 100%;
  width: 4px;
  left: 15%;
}
.strike-column-2 {
  height: 100%;
  width: 4px;
  left: 48%;
}
.strike-column-3 {
  height: 100%;
  width: 4px;
  left: 83%;
}
.strike-diagonal-1 {
  width: 90%;
  height: 4px;
  top: 50%;
  left: 5%;
  transform: skewY(45deg);
}
.strike-diagonal-2 {
  width: 90%;
  height: 4px;
  top: 50%;
  left: 5%;
  transform: skewY(-45deg);
}

.x-hover:hover::after {
  content: "X";
  opacity: 0.4;
}

.o-hover:hover::after {
  content: "O";
  opacity: 0.4;
}
Enter fullscreen mode Exit fullscreen mode

Our board would look like this

// Board.jsx

import Square from "./Square";
const Board = ({square, squareClick,strikeClass}) => {
  return (
<div className="flex flex-col">
   <div className="grid grid-cols-3 grid-rows-3 relative">
    <Square value={square[0]} onClick={() => squareClick(0)}/>
    <Square value={square[1]} onClick={() => squareClick(1)}/>
    <Square value={square[2]} onClick={() => squareClick(2)}/>
    <Square value={square[3]} onClick={() => squareClick(3)}/>
    <Square value={square[4]} onClick={() => squareClick(4)}/>
    <Square value={square[5]} onClick={() => squareClick(5)}/>
    <Square value={square[6]} onClick={() => squareClick(6)}/>
    <Square value={square[7]} onClick={() => squareClick(7)}/>
    <Square value={square[8]} onClick={() => squareClick(8)}/>
   </div>
// The strikeClass would come from Tic-tac-toe component 
   <div className={`absolute w-full bg-orange-600 z-40 ${strikeClass}`}></div>
  </div>
  );
};
export default Board;
Enter fullscreen mode Exit fullscreen mode

Step 7 Handling the winning combination and using the strikeClass based on the winning position

import Board from "./Board";
import { useState } from "react";

// Winner function 
const checkWinner = (square, setStrikeClass, setWinner) => {
// Multiple winning combination based on the position of the square and value
  const winningCombos = [
    { combo: [0, 1, 2], strikeClass: "strike-row-1" }, // top row
    { combo: [3, 4, 5], strikeClass: "strike-row-2" }, // middle row
    { combo: [6, 7, 8], strikeClass: "strike-row-3" }, // bottom row
    { combo: [0, 4, 8], strikeClass: "strike-diagonal-1" }, // top left to bottom right
    { combo: [2, 4, 6], strikeClass: "strike-diagonal-2" }, // top right to bottom left
    { combo: [0, 3, 6], strikeClass: "strike-column-1" }, // left column
    { combo: [1, 4, 7], strikeClass: "strike-column-2" }, // middle column
    { combo: [2, 5, 8], strikeClass: "strike-column-3" }, // right column
  ];
  for (const { combo, strikeClass } of winningCombos) {
    const [a, b, c] = combo;
    if (cells[a] && cells[a] === cells[b] && cells[a] === cells[c]) {
      setStrikeClass(strikeClass);
      if (cells[a] === "X") {
        setWinner("X");
      } else {
        setWinner("O");
      }
      return;
    }
  }
  const isDraw = square.every((square) => cell !== null);
  setWinner(isDraw ? "draw" : null);
};

const TicTacToe = () => {
const [square, setSquare] = useState(Array(9).fill(null));
const [player, setPlayer] = useState("X");
const [strikeClass, setStrikeClass] = useState("hidden");
const [winner, setWinner] = useState("");

const handleSquareClick = (index) => {
   // Return if square already contents a value
    if (square[index] !== null) return;
   // Create a new array to add each square index 
    const newSquare = [...square];
    newSquare[index] = player;
   // set square array to the new Array
    setSquare(newSquare);
   // switch between X and O based on player turn
    setPlayer(player === "X" ? "O" : "X");
  };

   // Track the winner
  useEffect(() => {
    checkWinner(square, setStrikeClass, setWinner);
  }, [square]);

  return (
    <div>
      <Board
        cells={cells}
        onClick={handleCellClick}
        strikeClass={strikeClass}
        player={player}
        winner={winner}
      />
    </div>
  );
};
export default TicTacToe;
Enter fullscreen mode Exit fullscreen mode

The entire code for each component and styling

// TicTacToe.jsx


import { useState, useEffect } from "react";
import Board from "./Board";

const checkWinner = (square, setStrikeClass, setWinner) => {
  const winningCombos = [
    { combo: [0, 1, 2], strikeClass: "strike-row-1" }, // top row
    { combo: [3, 4, 5], strikeClass: "strike-row-2" }, // middle row
    { combo: [6, 7, 8], strikeClass: "strike-row-3" }, // bottom row
    { combo: [0, 4, 8], strikeClass: "strike-diagonal-1" }, // top left to bottom right
    { combo: [2, 4, 6], strikeClass: "strike-diagonal-2" }, // top right to bottom left
    { combo: [0, 3, 6], strikeClass: "strike-column-1" }, // left column
    { combo: [1, 4, 7], strikeClass: "strike-column-2" }, // middle column
    { combo: [2, 5, 8], strikeClass: "strike-column-3" }, // right column
  ];
  for (const { combo, strikeClass } of winningCombos) {
    const [a, b, c] = combo;
    if (square[a] && square[a] === square[b] && square[a] === square[c]) {
      setStrikeClass(strikeClass);
      if (square[a] === "X") {
        setWinner("X");
      } else {
        setWinner("O");
      }
      return;
    }
  }
  const isDraw = square.every((square) => square !== null);
  setWinner(isDraw ? "draw" : null);
};

const TicTacToe = () => {
  const [square, setSquare] = useState(Array(9).fill(null));
  const [player, setPlayer] = useState("X");
  const [strikeClass, setStrikeClass] = useState("hidden");
  const [winner, setWinner] = useState("");

  const handleCellClick = (index) => {
    if (winner) return;
    if (square[index] !== null) return;
    const newSquare = [...square];
   newSquare[index] = player;
   setSquare(newSquare);
    setPlayer(player === "X" ? "O" : "X");
  };

  useEffect(() => {
    checkWinner(square, setStrikeClass, player, setWinner);
  }, [square]);

  const handleReset = () => {
    if (!winner && winner !== "draw") return;
    setSquare(Array(9).fill(null));
    setPlayer("X");
    setStrikeClass("hidden");
    setWinner("");
  };

  return (
    <div className="flex justify-center items-center mt-40">
      <Board
        square={square}
        onClick={handleCellClick}
        strikeClass={strikeClass}
        player={player}
        winner={winner}
        reset={handleReset}
      />
    </div>
  );
};

export default TicTacToe;
Enter fullscreen mode Exit fullscreen mode

// Board.jsx


import React from "react";
import Square from "./Square";

const Board = ({ square, onClick, strikeClass, player, winner, reset }) => {
  return (
    <div className="flex flex-col">
      <div className="grid grid-cols-3 grid-rows-3 relative">
        <Square
          value={square[0]}
          onClick={() => onClick(0)}
          className="border-r-8 border-b-8"
          player={player}
        />
        <Square
          value={square[1]}
          onClick={() => onClick(1)}
          className="border-r-8 border-b-8"
          player={player}
        />
        <Cell
          value={square[2]}
          onClick={() => onClick(2)}
          className="border-b-8"
          player={player}
        />
        <Square
          value={square[3]}
          onClick={() => onClick(3)}
          className="border-r-8 border-b-8"
          player={player}
        />
        <Square
          value={square[4]}
          onClick={() => onClick(4)}
          className="border-r-8 border-b-8"
          player={player}
        />
        <Square
          value={square[5]}
          onClick={() => onClick(5)}
          className="border-b-8"
          player={player}
        />
        <Square
          value={square[6]}
          onClick={() => onClick(6)}
          className="border-r-8"
          player={player}
        />
        <Square
          value={square[7]}
          onClick={() => onClick(7)}
          className="border-r-8"
          player={player}
        />
        <Square
          value={square[8]}
          onClick={() => onClick(8)}
          className=""
          player={player}
        />
        <div
          className={`absolute w-full bg-orange-600 z-40 ${strikeClass}`}></div>
      </div>
      {winner && (
        <div className="flex justify-center mt-16 border-2 p-4">
          <h3 className="text-4xl">
            {winner === "draw" ? "Its a Tie" : `Player ${winner} Wins!`}
          </h3>
        </div>
      )}
      <div className="flex justify-center mt-16 border-2 p-4 text-4xl">
        <button onClick={reset}>Reset</button>
      </div>
    </div>
  );
};

export default Board;
Enter fullscreen mode Exit fullscreen mode
import React from "react";

const Square = ({ onClick, value, className, player }) => {
  let hoverClass = null;
  if (value == null && player != null) {
    hoverClass = `${player.toLowerCase()}-hover`;
  }
  return (
    <button
      onClick={onClick}
      className={`w-28 h-28 text-5xl ${className} ${hoverClass}`}>
      {value}
    </button>
  );
};

export default Square;

Enter fullscreen mode Exit fullscreen mode

CSS


* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-size: 10px;
}
body {
background-color: #f5f5f5;
}

.strike-row-1 {
width: 100%;
height: 4px;
top: 15%;
}
.strike-row-2 {
width: 100%;
height: 4px;
top: 48%;
}
.strike-row-3 {
width: 100%;
height: 4px;
top: 83%;
}
.strike-column-1 {
height: 100%;
width: 4px;
left: 15%;
}
.strike-column-2 {
height: 100%;
width: 4px;
left: 48%;
}
.strike-column-3 {
height: 100%;
width: 4px;
left: 83%;
}
.strike-diagonal-1 {
width: 90%;
height: 4px;
top: 50%;
left: 5%;
transform: skewY(45deg);
}
.strike-diagonal-2 {
width: 90%;
height: 4px;
top: 50%;
left: 5%;
transform: skewY(-45deg);
}

.x-hover:hover::after {
content: "X";
opacity: 0.4;
}

.o-hover:hover::after {
content: "O";
opacity: 0.4;
}

Enter fullscreen mode Exit fullscreen mode




Conclusion:

Congratulations! You've successfully created a simple Tic Tac Toe game using React.js. This project covers the basics of React components, state management, and event handling. Feel free to enhance your game by adding features like a game history log, or styling improvements. Happy coding!

Top comments (0)