How to create a Tic Tac Toe Game using NEXT JS?

thumbnail

Hello friends, Today in this post we are going to see how we can create a tic tac toe game using NEXT JS. Tic Tac Toe is an excellent project for beginners to get started with learning NEXT/REACT JS, doing this project will help you learn basic concepts such as lifecycle methods, useEffect & useState hooks, functional component, etc. A Basic HTML, CSS & Javascript is enough to follow up with this tutorial. So get ready with your vscode and let's get started.

Step1 - Create a NEXT JS Project

Open vscode, click on terminal > new terminal or plus Ctrl + J and type below command

> npx create-next-app tic-tac-toe-game

Step2 - Writing JSX

Once project will be created, clean up some default created codes delete the Home.module.css under styles, under pages open index.js and remove everything under return statement and just add a div with h1 tag stating Tic Tac Toe Game

/pages/index.js
export default function Home() {
  return (
      <div>
        <h1>Tic Tac Toe</h1>
      </div>
  )
}

Note: Remember when we write html code in React/Next js it is termed as jsx (Javascript XML) is one of the feature of react which allows you to write html code in react and behind the scenes it will convert this html code to react element. Otherwise you would use the below syntax to create html element in React.

React.createElement('h1', props:{children: "Tic Tac Toe"})

Now we will create other elements for displaying game boards & menu

As we need 9 squares so we used Array(9) to create an empty array with length as 9 and we iterated by using spread operator [...Array(9)] and map function to access value and idx and for each iteration we created a sqaure

/pages/index.js
export default function Home() {
  return (
    <div>
      <h1>Tic Tac Toe</h1>
      <div className="game">
        <div className="game__menu">
          <p>xTurn</p>
        </div>
        <div className="game__board">
          {[...Array(9)].map((v, idx) => {
            return (
              <div

                key={idx}
                className="square">
                X
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

Now apply styles for the elements created in global.css

html,
body {
  padding: 0;
  margin: 0;
  font-family: "Courier New", Courier, monospace;
}

a {
  color: inherit;
  text-decoration: none;
}

* {
  box-sizing: border-box;
}

h1 {
  text-align: center;
}

.game {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.game__menu {
  text-align: center;
  font-size: 24px;
  font-weight: 600;
}

.game__board {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-gap: 8px;
}

.square {
  background-color: #eee;
  border-radius: 8px;
  box-shadow: 0px 4px #ddd;
  text-align: center;
  font-size: 64px;
  line-height: 100px;
  font-weight: bold;
  width: 100px;
  height: 100px;
  cursor: pointer;
}

Step3: Adding X & O to game board

Next we will work on adding X & O alternate for that we initialize two state variables so here we will use useState hook as below.

/pages/index.js

import { useState } from "react";
export default function Home() {
  const [xTurn, setXTurn] = useState(true);
  const [boardData, setBoardData] = useState({
    0: "",
    1: "",
    2: "",
    3: "",
    4: "",
    5: "",
    6: "",
    7: "",
    8: "",
  });
  ...
  ..
  .
}

We are storing boolean value for xTurn to determine whoose turn and boarddata object for storing values for squares initialially it is empty for all index.

Create a updateBoardData arrow function with idx argument and inside that function add a if condition to check whether the index in boardData is empty or not if empty then setBoardData with value for passed index with X or O and finally revert the xTurn by setting xTurn to !xTurn. Now add a onclick listener to the square div and call this function and assign the boardData value to the square based on the index. Now you will be add X & O on the square.

/pages/index.js
...
export default function Home() {
  ...
  const updateBoardData = (idx) => {
    if (!boardData[idx]) {
      //will check whether specify idx is empty or not
      let value = xTurn === true ? "X" : "O";
      setBoardData({ ...boardData, [idx]: value });
      setXTurn(!xTurn);
    }
  };
  ....
  return (
    <div>
      <h1>Tic Tac Toe</h1>
      <div className="game">
        <div className="game__menu">
          <p>{xTurn === true ? "X Turn" : "O Turn"}</p>
        </div>
        <div className="game__board">
          {[...Array(9)].map((v, idx) => {
            return (
              <div

                key={idx}
                className="square"
                onClick={() => {
                  updateBoardData(idx);
                }}>
{boardData[idx]}
             </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

post3 (1).webp

Step4: Adding winning logic

Now we will write winning logic for the game. As we know that player will won if we have all X or O in the same row or column and diagonal so based on that we will form a 2d array.

const WINNING_COMBO = [
  [0, 1, 2],
  [3, 4, 5],
  [6, 7, 8],
  [0, 3, 6],
  [1, 4, 7],
  [2, 5, 8],
  [0, 4, 8],
  [2, 4, 6],
];

So to track the winning status we will create a boolean state variable named won with initial value as false. Create a checkWinner function, iterate through WINNING_COMBO 2D Array using map function, first we will assign value to a, b, c using array destructuring for eg: we have array with value [1, 3, 5] so if we want to assign it to a, b, c then we will assign it as below

let [a, b, c] = [1, 3, 5]

now a = 1, b = 3, c = 5, we followed the same method here too, now we will add a condition to check we have same values on this combo if yes then we set won to true.

/pages/index.js
...
export default function Home() {
  const [won, setWon] = useState(false);
  ...
 const checkWinner = () => {
    WINNING_COMBO.map((bd) => {
      const [a, b, c] = bd;
      if (
        boardData[a] &&
        boardData[a] === boardData[b] &&
        boardData[a] === boardData[c]
      ) {
        setWon(true);

      }
    });
  };
}

Now we have function ready, so we have to call this function whenever board data is updated so here comes to rescue is useEffect hook so basically useEffect will help in writing component lifecycle like what to do when component is loaded/mounted, what to do when component updated etc. Do you know ? Everytime you call this setBoardData, setWon.... your component gets unmounted and re-rendered, this helps react dynamically update the component.

So syntax for useEffect is

useEffect(()=>{
  //your function to be executed
  return () => {
    //cleanup code / unmount code
  }
}, [dependency1, dependency2, ....]);

So when we specify any dependency then function inside will be called whenever dependency gets updated, if dependency list is empty then it will called after every state update.

Suppose if we want to call function only once when component is loaded then just remove the dependency array.

Note there is difference between this two syntax

// will be executed only once

useEffect(()=>{
  //your function to be executed
  return () => {
    //cleanup code / unmount code
  }
});

// will be executed on every update
useEffect(()=>{
  //your function to be executed
  return () => {
    //cleanup code / unmount code
  }
}, []);

So we want to call check winner everytime our boardData is updated so our dependency would be boardData and call checkWinner() inside useEffect we dont have any cleanup code for this we will remove return I will have a post regarding the usage of this cleanup in my future so stay tuned.

So coming back here our code will be like this now, for better clarity we will create a paragraph graph dislaying the win status. Also add a !won condition to updateBoardData before updaing xTurn and boardData so that gameboard will not accept input after winning. Now try playing the game and you will see the Game won will be true

import { useEffect, useState } from "react";

const WINNING_COMBO = [
  [0, 1, 2],
  [3, 4, 5],
  [6, 7, 8],
  [0, 3, 6],
  [1, 4, 7],
  [2, 5, 8],
  [0, 4, 8],
  [2, 4, 6],
];
export default function Home() {
  const [xTurn, setXTurn] = useState(true);
  const [won, setWon] = useState(false);

  const [boardData, setBoardData] = useState({
    0: "",
    1: "",
    2: "",
    3: "",
    4: "",
    5: "",
    6: "",
    7: "",
    8: "",
  });
  useEffect(() => {
    checkWinner();
  }, [boardData]);
  const updateBoardData = (idx) => {
    if (!boardData[idx] && !won) {
      //will check whether specify idx is empty or not
      let value = xTurn === true ? "X" : "O";
      setBoardData({ ...boardData, [idx]: value });
      setXTurn(!xTurn);
    }
  };
  
  const checkWinner = () => {
    WINNING_COMBO.map((bd) => {
      const [a, b, c] = bd;
      if (
        boardData[a] &&
        boardData[a] === boardData[b] &&
        boardData[a] === boardData[c]
      ) {
        setWon(true);
      }
    });
  };
 
  return (
    <div>
      <h1>Tic Tac Toe</h1>
      <div className="game">
        <div className="game__menu">
          <p>{xTurn === true ? "X Turn" : "O Turn"}</p>
          <p>{`Game Won:${won}`}</p>
        </div>
        <div className="game__board">
          {[...Array(9)].map((v, idx) => {
            return (
              <div
                onClick={() => {
                  updateBoardData(idx);
                }}
                key={idx}
                className="square"
              >
                {boardData[idx]}
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

Step5 - Game Draw Logic

It's a very logic we have to check whether every value on boardData is not empty. So we will have boolean state variable named draw and initialize to false and we will use Object.keys to return every key and use every function to check whether it is non empty. Basically this every function will return true if condition is true even if one condition fails then it will return false.

Again use paragraph tag to display draw status.

const checkDraw = () => {
    let check = Object.keys(boardData).every((v) => boardData[v]);
    if (check) setIsDraw(check);
  
};

<div className="game__menu">
          <p>{xTurn === true ? "X Turn" : "O Turn"}</p>
          <p>{`Game Won: ${won} | Game Draw: ${isDraw}`}</p>
 </div>

post3 (3).webp

Step6 - Final Touch

We will add some minor touches like showing a modal when a player won or match draw, reset button and highlight the squares which made to won. For highlighing we will create a wonCombo state variable which will be list of 3 index which leads to win. So under you checkWinner() function after setting won to true set the wonCombo to current iteration on the WINNING_COMBO. Now, in your square div add className highlight if you wonCombo include this square index.

const checkDraw = () => {
    let check = Object.keys(boardData).every((v) => boardData[v]);
    setIsDraw(check);
    if (check) setModalTitle("Match Draw!!!");
  };
  const checkWinner = () => {
    WINNING_COMBO.map((bd) => {
      const [a, b, c] = bd;
      if (
        boardData[a] &&
        boardData[a] === boardData[b] &&
        boardData[a] === boardData[c]
      ) {
        setWon(true);
        setWonCombo([a, b, c]);
        setModalTitle(`Player ${!xTurn ? "X" : "O"} Won!!!`);

        return;
      }
    });
  };

For modal we will maintain a boolean variable to show and hide, and modalTitle to show won or draw. create a reset function to make every variable back to its initial state and call this function on new game button clicked inside the modal.

const reset = () => {
    setBoardData({
      0: "",1: "",2: "",3: "",4: "",5: "",6: "",7: "",8: "",
    });
    setXTurn(true);
    setWon(false);
    setWonCombo([]);
    setIsDraw(false);
    setModalTitle("");
};
<div className={`modal ${modalTitle ? "show" : ""}`}>
   <div className="modal__title">{modalTitle}</div>
   <button onClick={reset}>New Game</button>
</div>

Also add the css for this elements in global.css.

.square.highlight {
  background-color: aquamarine;
  box-shadow: none;
}

.modal {
  width: 250px;
  border-radius: 16px;
  box-shadow: 0px 0px 10px 0px gray;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 24px;
  position: fixed;
  top: 50%;
  background-color: white;
  left: 50%;
  transform: translate(-50%, -50%) scale(0);
  transition: transform 0.2s;
}

.modal.show {
  transform: translate(-50%, -50%) scale(1);
}

.modal__title {
  font-size: 18px;
  font-weight: bold;
  margin-bottom: 16px;
}

button {
  border: none;
  width: 100%;
  height: 36px;
  font-size: 18px;
  font-weight: 600;
}

So now if we won now the squares will be updated based on win combo and modal will be shown based on win or draw, and game will be reset once "New Game" is clicked.

post3 (4).webp

That's it from my end friends, once you completed this project you would have definitely learned some basic concepts. Just to challenge your understanding try implementing a feature that asks whether player want 'X' or 'O' at game start and add a scoring system to count X and O winning count.

Please find the final code in Github.

https://github.com/CodeWithMarish/tic-tac-toe

If you found this post helpful please share maximum, Thanks for reading 😊 Stay tuned.

Also don't forgot to subscribe our youtube channel codewithmarish for all web development related challenges.

https://www.youtube.com/channel/UCkPYmdVz8aGRH6qCdKMRYpA

Visit React JS official documentation for more on react hooks:

https://reactjs.org/docs/hooks-overview.html

Related Posts