Everyone has played tic-tac-toe at least once, but have you ever created a tic-tac-toe board using React? No? Well then, it's your lucky day! We'll create one from scratch using React, context, useEffects, and some cool features that React provides for us.

Prerequisites

You'll need to have React ready to go before you can build the board, but if it isn't yet installed, don't worry! You just need to install NodeJS and NPM. This is the official NodeJS website, where you can download the best version for you.

 Note: If you are using a Mac, I recommend that you install it using Homebrew.

Step 1: Create the App

I'll assume that you now have both NodeJS and NPM installed, so let's go ahead and create our app! Start by opening your terminal and choosing where you want to place your application, then type this command:

npx create-react-app tictactoe

 

What does this command do?

The npx is the package that usually comes with NodeJS, and it has the create-react-app, which is self-explanatory, but I'll say it anyway: it creates a project with all necessary dependencies for a React application. Finally, the tictactoe is the project name, so you can change it as you want or even create it under a structure like myapps/tictactoe.

Now that your project is created, you can run NPM start from the terminal and you'll see a screen like this in your localhost:3000:

Step 2: Create Components

For this game, I decided to create components for the board and the cells. You can think of each spot in the tic-tac-toe board as a cell. In order to create these cells, let's create a folder named "components" under src and the files Board.jsx and Cell.jsx under it, as shown below.

Each cell must now know which spot it represents, so let's create a multidimensional array for it. The first cell will be 0-1, the second cell in the same row will be 0-2, the first cell of the second row will be 1-1, and so on:

import React, { useContext } from "react";

function Cell({row, column}) {
  return (
      <div>
          <div>?</div>
      </div>
  )
}
export default Cell

For the board, we will include 9 cells since we have 9 places on our tic-tac-toe board:

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

function Board() {
 
  return (
      <div className="board">
          <Cell row={0} column={0} />
          <Cell row={0} column={1} />
          <Cell row={0} column={2} />
         
          <Cell row={1} column={0} />
          <Cell row={1} column={1} />
          <Cell row={1} column={2} />

          <Cell row={2} column={0} />
          <Cell row={2} column={1} />
          <Cell row={2} column={2} />
     </div>
  )
}

export default Board

Don't worry about the className for now; we'll create styles for the components at the end.

Let's also create some other components to represent the headers to show who's playing, who wins, and if the game has ended. We can do this by creating components called End.jsx, Header.jsx, Playing.jsx, and Winner.jsx. We'll create code for them pretty soon.

Step 3: Add Some Logic!

Now let's add logic to the main file of the application, the App.js. In the App.js, you will see some code that you can go ahead and wipe out since we'll be creating new code.

Let's start with our imports. In App.js, you can replace the imports with these:

import Board from './components/Board'
import './App.css';
import React, { useState , createContext, useEffect } from 'react';
import Header from './components/Header';


export const AppContext = createContext();

We'll use the useState, createContext, and useEffect in the App.js file.

All the code now should be created inside the function App(). Let's start with the variables that we'll need:

const emptyGame = [["", "", ""], ["", "", ""], ["", "", ""]] // empty board

const [cells, setCells] = useState(emptyGame)
const [winnerCells, setWinnerCells] = useState([[],[],[]]) // this will be the final touch

const X = "X" // I like to use constants rather that the character directly
const O = "O"
const [currentChar, setCurrentChar] = useState(X); // X starts first
const [winner, setWinner] = useState("");
const [gameOver, setGameOver] = useState(false) // bool to sign that the game is over

Now let's create the cell behavior. When the user clicks on a cell, this function must be triggered:

function cellClick(row, column) {
  if (gameOver || winner) { // do nothing if the game is over or there's a winner
    return;
  }

  if (cells[row][column] != "") { // cell isn't empty? Do nothing!
    return;
  }

   // if it reaches here means that the click is valid
  const newBoard = {...cells} // javascript way to copy an array
  newBoard[row][column] = currentChar // Do you remember the 0-1, 1-1 structure?
  setCells(newBoard) // set the cells with the new value


   // Change the current player

   if (currentChar == X) {
    setCurrentChar(O)
  } else {
    setCurrentChar(X)
  }
}

Now we have the function, but we aren't triggering it anywhere. We have to add it in the Cell.jsx file, and in order to do this, we'll use the useContext that we have imported because Context is a simple way to pass variables through components.

Replace the return (...) as shown below:

return (
  <div className="App">
    <header className="App-header">
      <AppContext.Provider value=>
        <Header />
        <Board />
      </AppContext.Provider>

      <button className='btn-reset' onClick={() => reset()}>Reset</button>
    </header>
  </div>
);

 

AppContext is the name we defined for the context right after the imports.

We have to wrap in it the components that will have access to this context.

Last but not least, we pass in the value, the functions, and the variables we want to give access to.

You may have noticed that I added a button to reset the board. I won't show it here because I want to challenge you to create it on your own, but you can reference my GitHub and view the whole project there with this function if you want.

In the Cell.jsx file, we'll now have to get the context. To do so, we'll need to import the context and use it as shown below in the final version of Cell.jsx:

import React, { useContext } from "react";
import { AppContext } from "../App";
import "../style/cell.css" // this will be created in the next section

function Cell({row, column}) {
  const { cells, cellClick, gameOver, winnerCells } = useContext(AppContext)
  const currentVal = cells[row][column]

   // active and winner are used for style purposes
  return (
      <div className={"cell"
        + (!currentVal && !gameOver ? " active" : "")
        + (winnerCells[row][column] ? " winner" : "")
        + (gameOver ? " disabled" : "")}
        onClick={() => cellClick(row, column)}>
          <div>{currentVal}</div>
      </div>
  )
}

export default Cell

We are almost there, so stick with me!

Let's go back to our App.js file and create the functions to end the game:

// instead of checking cell[0][0] is equal to cell[0][1] and so on,

// I created a function to check all values in the same row

// and another function to check all values in the same column

function isGameOver() {
  switch (true) {
    case areTheSameInRow(0): return
    case areTheSameInRow(1): return
    case areTheSameInRow(2): return
    case areTheSameInColumn(0): return
    case areTheSameInColumn(1): return
    case areTheSameInColumn(2): return
    case areTheSameInDiagonal(): return
  }

   // this is going to check if we still have empty spots
  if (!cells[0].includes("") && !cells[1].includes("") && !cells[2].includes("")) {
    endGame("")
  }
}

function endGame(winner) {
  if (winner != "") setWinner(winner)
  setGameOver(true)
}

 // it receives the row and checks if all values in that row are the equal
function areTheSameInRow(row) {
  if (cells[row][0] !== "" && cells[row][0] === cells[row][1] && cells[row][0] === cells[row][2]) {
    endGame(cells[row][0])
   
    const newWinner = [[],[],[]]
    newWinner[row][0] = true
    newWinner[row][1] = true
    newWinner[row][2] = true
    setWinnerCells(newWinner) // Styling purposes
    return true
  }
  return false
}

 // it receives the column and checks if all values in that column are the equal
function areTheSameInColumn(column) {
  if (cells[0][column] !== "" && cells[0][column] === cells[1][column] && cells[2][column] == cells[0][column]) {
    endGame(cells[0][column])
   
    const newWinner = [[],[],[]]
    newWinner[0][column] = true
    newWinner[1][column] = true
    newWinner[2][column] = true
    setWinnerCells(newWinner) // Styling purposes
    return true
  }
  return false
}

function areTheSameInDiagonal() {
  if (cells[1][1] != "" && (cells[0][0] === cells[1][1] && cells[0][0] == cells[2][2])) {
    endGame(cells[1][1])
    const newWinner = [[],[],[]]
    newWinner[0][0] = true
    newWinner[1][1] = true
    newWinner[2][2] = true
    setWinnerCells(newWinner) // Styling purposes
    return true
  }

  if (cells[1][1] != "" && (cells[2][0] === cells[1][1] && cells[2][0] == cells[0][2])) {
    endGame(cells[1][1])
    const newWinner = [[],[],[]]
    newWinner[0][2] = true
    newWinner[1][1] = true
    newWinner[2][0] = true
    setWinnerCells(newWinner) // Styling purposes
    return true
  }
  return false
}

Are we missing something?

Yes, we are! We're missing the files End.jsx, Header.jsx, Playing.jsx, and Winner.jsx, so let's code them!

Header:

import React, { useContext, useState } from "react";
import { AppContext } from "../App";
import '../style/header.css'
import Playing from "./Playing";
import End from "./End";
import Winner from "./Winner";

function Header() {
 
  const { currentChar, winner, gameOver } = useContext(AppContext)

   // This is equivalent of a ternary if to conditionally show something

   // { !winner && !gameOver && <Playing /> }

   // it will show the Playing component if has no winner and the have not ended
  return (
      <div className="header">
          { !winner && !gameOver && <Playing /> }
          { gameOver && !winner && <End /> }
          { winner && <Winner /> }
      </div>
  )
}

export default Header

Winner:

import React, { useContext, useState } from "react";
import { AppContext } from "../App";

function Winner() {
 
  const { winner } = useContext(AppContext)

  return (
      <div>Congratulations <span>{winner}</span>, you WON!</div>
  )
}

export default Winner

End:

import React from "react";

function End() {
 
  return (
      <div>Game Over</div>
  )
}

export default End

Playing:

import React, { useContext, useState } from "react";
import { AppContext } from "../App";

function Playing() {
 
  const { currentChar } = useContext(AppContext)

  return (
      <div>Playing now: <span>{currentChar}</span></div>
  )
}

export default Playing

Everything looks fine, but when and where do we call the isGameOver() function?

Well, let's create an effect for that!

The useEffect applies some function every time the "spied" object changes.

Create the following code inside the function App():

useEffect(function() {
  isGameOver()
}, [cells]);

This will run the isGameOver() function every time we change the "cells" constant.

Step 4: Add Styling

Our project is functional, but it does not look great. Just run it and you'll see how awful it is. Let's fix this!

First, create a folder named "style" under the src folder, then create three files for the board, cell, and header.

You can tweak it as you want, but here's how I did it:

board.css:

.board {
  height: 380px;
  width: 380px;
  background-color: #fff;
  border-radius: 5px;
  display: grid;
  grid-template-columns: 120px 120px 120px;
  grid-gap: 10px;
  padding: 10px;
}

cell.css:

.cell {
  width: 120px;
  height: 120px;
  font-size: 2.2em;
  background-color: #282c34;
}

.cell div {
  margin-top: 20px;
}

.cell.active {
  cursor: pointer;
}

.cell.disabled {
  background-color: rgb(125, 125, 125);
}

.cell.winner {
  background-color: rgb(177, 238, 175);
  color: rgb(171, 24, 24);
}

header.css:

.header {
  font-size: 1.5em;
  margin-bottom: 5px;
}

.header div span {
  color: rgb(177, 238, 175);
}

Now our game not only works well, but it also looks great!

Conclusion

You can find the complete project on my GitHub page.

There are thousands of ways to create a tic-tac-toe game, so tell me: what would you do differently from this approach? How would you improve this code?


Author

Diego Zanivan

Diego Zanivan is a Back-End Developer at Avenue Code. He's a cutting-edge technology enthusiast who's been passionate about developing awesome tools for over 15 years. Diego usually works with PHP and Java but also fills in as the Front-End guy sometimes.


Asymmetric Cryptography

READ MORE

Improving the developer experience when documenting

READ MORE

How to Run Rust from Python

READ MORE