DEV Community

loading...

Creating Tic-Tac-Toe Using React / JavaScript

Tim Winfred (He/She/They)
💻 Front-End && JavaScript Web Developer | Accessibility Evangelist | Bootcamp Grad | Eternal Student | Artist && Designer 🤓
・3 min read

As I continued my #100DaysOfCode challenge tonight, I decided to test my React skills to see if I could create the classic children's game Tic-Tac-Toe.

From start to finish, I believe the challenge took me about two hours, although the first 15 minutes was prepping how I wanted to design my code.

These were my pre-coding notes:

Use React

Create GameBoard component
Create GameRow component
Create GameSquare component (button)

State will live in the GameBoard component

State will include a 3x3 2D array that represents the board
- 0 = unfilled
- 1 = X
- 2 = O

State will include a moves counter that increments every time a move is made

Every time a player clicks on a GameSquare button, it sends an onClick up to parent component
Modifying the state will rerender the GameSquare component to visually show X or O

Every time a player makes a move, increment the move counter and check the move counter amount
If the counter is at least 5, check the win conditions
Only check win conditions related to the location that was updated (see below)

Win conditions:
- all items in a row
- all items in a column
- all items diagonally

Win conditions stored in a hash table (object)
- the keys would be the location of each square
>> i.e. [0,0], [0,1], [0,2], [1,0], [1,1], etc...
- values would be possible win directions for the key
>> i.e. [0,0] win conditions would be [[0,1],[0,2]], [[1,0],[2,0]], [[1,1],[2,2]]

If a win condition is ever satisfied, send an alert announcing who won and reset state
Enter fullscreen mode Exit fullscreen mode

The hardest part of this challenge was figuring out how to handle the win conditions. I still think there is probably an algorithmic way to code the winConditions, but that felt more like a "nice-to-have". Maybe I'll end up updating it in the future, who knows =)

I'd love any feedback on my code, which I've pasted below for convenience. Drop me a comment if you have any thoughts!

The biggest issue I ran into was even though the gameBoard state was being updated when a GameBoard button was clicked, the DOM wasn't updating to reflect the change. After some sleuthing, I discovered that this was happening because I was originally just passing gameBoard into updateGameBoard (Gameboard.js - line 51). The children components weren't updating because React was seeing it as the same array (even though elements inside of it were updated). In the end, I had to spread the array into a new array to force it to update. Worked like a charm!

Here is the final code:

// GameBoard.js
import { useState, useEffect } from 'react';
import GameRow from './GameRow';

function App() {
  const [gameBoard, updateGameBoard] = useState([[0, 0, 0], [0, 0, 0], [0, 0, 0]]);
  const [winner, updateWinner] = useState();
  const [turnCounter, updateTurnCounter] = useState(1);
  const [currentPlayer, updateCurrentPlayer] = useState(1);

  useEffect(() => {
    if (winner) {
      alert(`Congrats player ${winner}, you're the winner!`);
      updateGameBoard([[0, 0, 0], [0, 0, 0], [0, 0, 0]]);
      updateWinner(null);
      updateTurnCounter(1);
      updateCurrentPlayer(1);
    }
  }, [winner]);

  const isWinner = (location) => {
    const winConditions = {
      '0,0': [[[0,1],[0,2]], [[1,0],[2,0]], [[1,1],[2,2]]],
      '0,1': [[[0,0],[0,2]], [[1,1],[2,1]]],
      '0,2': [[[0,0],[0,1]], [[1,2],[2,2]], [[1,1],[2,0]]],
      '1,0': [[[1,1],[1,2]], [[0,0],[2,0]]],
      '1,1': [[[0,1],[2,1]], [[1,0],[1,2]], [[0,0],[2,2]], [[0,2],[2,0]]],
      '1,2': [[[1,0],[1,1]], [[0,2],[2,2]]],
      '2,0': [[[0,0],[1,0]], [[2,1],[2,2]], [[1,1],[0,2]]],
      '2,1': [[[0,1],[1,1]], [[2,0],[2,2]]],
      '2,2': [[[0,2],[1,2]], [[2,0],[2,1]], [[0,0],[1,1]]]
    };

    let winner = false;

    winConditions[location].forEach(winLocation => {
      const isWinner = winLocation.every(item => {
        return gameBoard[item[0]][item[1]] === currentPlayer;
      });

      if (isWinner) {
        winner = true;
        return;
      }
    });

    return winner;
  }

  const handleGameSquareClick = (location) => {
    gameBoard[location[0]][location[1]] = currentPlayer;
    updateGameBoard([...gameBoard]);

    if (turnCounter > 4) {
      const weHaveAWinner = isWinner(location);

      console.log('do we have a winner?', weHaveAWinner);
      if (weHaveAWinner) {
        console.log('updating winner')
        updateWinner(currentPlayer);
      }
    }

    updateCurrentPlayer(currentPlayer === 1 ? 2 : 1);
    updateTurnCounter(turnCounter + 1);
  }

  return (
    <div className="App">
      <h1>TIM Tac Toe</h1>
      <h2>Player {currentPlayer}'s turn</h2>
      {
        gameBoard.map((row, index) => (
          <GameRow row={row} rowIndex={index} key={index} handleClick={handleGameSquareClick}/>
        ))
      }
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode
// GameRow.jsx
import GameSquare from './GameSquare';

function GameRow({ row, ...props }) {
  return (
    <div>
      {
        row.map((square, index) => (
          <GameSquare square={square} columnIndex={index} key={index} {...props} />
        ))
      }
    </div>
  )
}

export default GameRow;
Enter fullscreen mode Exit fullscreen mode
import './GameSquare.scss';

function GameSquare({ square, handleClick, rowIndex, columnIndex }) {
  return (
    <button onClick={() => handleClick([rowIndex, columnIndex])}>
      {
        !square ? '' : (square === 1 ? 'X' : 'O')
      }
    </button>
  )
}

export default GameSquare;
Enter fullscreen mode Exit fullscreen mode

Discussion (1)