Creating Tic-Tac-Toe Using React / JavaScript
Tim Winfred (They/He)
Posted on June 8, 2021
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
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;
// 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;
import './GameSquare.scss';
function GameSquare({ square, handleClick, rowIndex, columnIndex }) {
return (
<button onClick={() => handleClick([rowIndex, columnIndex])}>
{
!square ? '' : (square === 1 ? 'X' : 'O')
}
</button>
)
}
export default GameSquare;
Posted on June 8, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.