In this blog, we will walk through the code to build a Tic-Tac-Toe game using React, step by step. By the end, you'll understand how to manage state, handle user interactions, and implement game logic in a React component.
Step 1: Setting Up the Component
First, let's create the main component for our Tic-Tac-Toe game.
1"use client";
2
3import { useEffect, useState } from "react";
4import "./TicTacToe.css";
5
6export default function TicTacToe() {
7 const rows = 3;
8 const cols = 3;
9
10 type ClickedCells = {
11 [key: string]: string;
12 };
13
14 const [clickedCell, setClickedCell] = useState<ClickedCells>({});
15 const [boolX, setBoolX] = useState(false);
16 const [boolO, setBoolO] = useState(false);
17 const [logArr, setLogArr] = useState<boolean>(false);
18 const [firstClick, setFirstClick] = useState(false);
19 const [arr, setArr] = useState<string[]>([]);
20 const [arrO, setArrO] = useState<string[]>([]);
21 const [currentPlayer, setCurrentPlayer] = useState<string>("X");
22
23 // Create a function to generate the matrix with coordinates
24 const createMatrix = (rows: number, cols: number) => {
25 const matrix = [];
26 for (let i = 0; i < rows; i++) {
27 const row = [];
28 for (let j = 0; j < cols; j++) {
29 row.push(`${i},${j}`);
30 }
31 matrix.push(row);
32 }
33 return matrix;
34 };
35
36 const matrix = createMatrix(rows, cols);
37
38 // Rest of the code
39}
40
41
Here, we've set up the initial state variables and created a function to generate a 3x3 matrix, representing the Tic-Tac-Toe board.
Step 2: Handling Click Events
Next, we handle the click events on the cells. When a cell is clicked, we update the state to mark the cell with the current player's symbol (X or O).
1const handleClick = (position: string) => {
2 setClickedCell((prev) => ({
3 ...prev,
4 [position]: currentPlayer,
5 }));
6 if (!firstClick) {
7 setArr((prevArr) => {
8 const newArr = [...prevArr, position];
9 return newArr;
10 });
11 } else {
12 setArrO((prevArr) => {
13 const newArrO = [...prevArr, position];
14 return newArrO;
15 });
16 }
17 setLogArr(true);
18 setCurrentPlayer(currentPlayer === "X" ? "O" : "X"); // Toggle player
19 setFirstClick(!firstClick);
20};
21
22
This function updates the clickedCell state with the current player's move and toggles the player for the next turn.
Step 3: Checking for a Winner
We use the useEffect hook to check if there's a winning combination after every move.
1useEffect(() => {
2 const winningArrays = [
3 ["0,0", "0,1", "0,2"], // Row 1
4 ["1,0", "1,1", "1,2"], // Row 2
5 ["2,0", "2,1", "2,2"], // Row 3
6 ["0,0", "1,0", "2,0"], // Column 1
7 ["0,1", "1,1", "2,1"], // Column 2
8 ["0,2", "1,2", "2,2"], // Column 3
9 ["0,0", "1,1", "2,2"], // Diagonal from top-left to bottom-right
10 ["0,2", "1,1", "2,0"], // Diagonal from top-right to bottom-left
11 ];
12
13 if (logArr) {
14 const hasWonX = winningArrays.some((winningArray) =>
15 winningArray.every((item) => arr.includes(item))
16 );
17
18 const hasWonO = winningArrays.some((winningArray) =>
19 winningArray.every((item) => arrO.includes(item))
20 );
21
22 if (hasWonX) {
23 setBoolX(true);
24 }
25 if (hasWonO) {
26 setBoolO(true);
27 }
28 setLogArr(false); // Reset the flag after logging
29 }
30}, [arr, arrO, logArr]);
31
32
This effect runs every time the arr, arrO, or logArr state changes. It checks if either player has a winning combination and updates the corresponding state variables.
Step 4: Rendering the Game Board
Finally, we render the game board and display the winner if there is one.
1const renderMatrix = () => {
2 return (
3 <div className="game">
4 <div className="grid">
5 {matrix.flat().map((item, index) => (
6 <button
7 key={index}
8 className="cell"
9 onClick={() => handleClick(item)}
10 >
11 {clickedCell[item] ? clickedCell[item] : ""}
12 </button>
13 ))}
14 </div>
15 {boolX && <div className="winner">X won</div>}
16 {boolO && <div className="winner">O won</div>}
17 </div>
18 );
19};
20
21return <>{renderMatrix()}</>;
22
23
Here, we map through the matrix to render buttons for each cell. When a cell is clicked, it triggers the handleClick function. We also display a message when a player wins.
Step 5: Styling the Game
To make the game look better, we add some CSS.
1/* TicTacToe.css */
2:root {
3 --background-color: #ffffff;
4 --text-color: #000000;
5 --border-color: #000000;
6 --cell-background: #f0f0f0;
7 --cell-hover-background: #e0e0e0;
8 --winner-color: #ff0000;
9}
10
11@media (prefers-color-scheme: dark) {
12 :root {
13 --background-color: #333333;
14 --text-color: #ffffff;
15 --border-color: #ffffff;
16 --cell-background: #444444;
17 --cell-hover-background: #555555;
18 }
19}
20
21body {
22 background-color: var(--background-color);
23 color: var(--text-color);
24 font-family: Arial, sans-serif;
25 display: flex;
26 justify-content: center;
27 align-items: center;
28 height: 100vh;
29 margin: 0;
30}
31
32.grid {
33 display: grid;
34 grid-template-columns: repeat(3, 1fr);
35 grid-template-rows: repeat(3, 1fr);
36 gap: 5px;
37 width: 160px;
38 height: 160px;
39 border: 2px solid var(--border-color);
40 background-color: var(--border-color);
41}
42
43.cell {
44 display: flex;
45 align-items: center;
46 justify-content: center;
47 background-color: var(--cell-background);
48 color: var(--text-color);
49 width: 50px;
50 height: 50px;
51 font-size: 24px;
52 font-weight: bold;
53 cursor: pointer;
54 border: none;
55 outline: none;
56 transition: background-color 0.3s;
57}
58
59.cell:hover {
60 background-color: var(--cell-hover-background);
61}
62
63.cell:nth-child(1),
64.cell:nth-child(2),
65.cell:nth-child(3) {
66 border-bottom: 2px solid var(--border-color);
67}
68
69.cell:nth-child(1),
70.cell:nth-child(4),
71.cell:nth-child(7) {
72 border-right: 2px solid var(--border-color);
73}
74
75.cell:nth-child(3),
76.cell:nth-child(6),
77.cell:nth-child(9) {
78 border-left: 2px solid var(--border-color);
79}
80
81.cell:nth-child(7),
82.cell:nth-child(8),
83.cell:nth-child(9) {
84 border-top: 2px solid var(--border-color);
85}
86
87.winner {
88 color: var(--winner-color);
89 margin-top: 20px;
90 font-size: 20px;
91 font-weight: bold;
92}
93