If you're new to React and want a quick overview of the library, then following this tutorial on their official website could really be helpful. The tutorial explains how to build a tic-tac-toe game using React and there are some further improvements that are left as an exercise to the readers. Here I am going to explain my solution to those listed improvements.
git clone https://github.com/ssmkhrj/tic-tac-toe-react.git
cd tic-tac-toe-react
npm install
npm startThis is where the official React tutorial ended. So, we will be starting from here onwards.
- Display the location for each move in the format (col, row) in the move history list
- Bold the currently selected item in the move list
- Rewrite Board to use two loops to make the squares instead of hardcoding them
- Add a toggle button that lets you sort the moves in either ascending or descending order
- When someone wins, highlight the three squares that caused the win
- When no one wins, display a message about the result being a draw
In this improvement we need to add the location where the move occurred along with the move number. So, if our first move is at square (1,1), then our button should say Go to #1 At: (1,1).
Following are the changes that we make in order to achieve this:
- In the
Gamecomponent every item in thehistorystate stores a snapshot of the board which is stored in thesquaresproperty. In addition to thesquaresproperty we add alocationproperty to store the location where the move was made. So our state now looks like this:
this.state = {
history: [
{
squares: Array(9).fill(null),
location: null,
},
],
stepNumber: 0,
xIsNext: true,
};- Then we modify the
handleClickmethod to update the location accordingly. We make use of theiparameter to get the location at which the click happened, but sinceiis the one-dimensional index we need to break it down into row and column position. So ourhandleClickmethod now looks like this.
handleClick(i) {
...
const row = Math.floor(i / 3) + 1;
const col = (i % 3) + 1;
this.setState({
history: history.concat([
{
squares: squares,
location: `(${row},${col})`,
},
]),
...
});
}- Finally we update the
rendermethod to display the location. This how it looks after the update.
render() {
...
const moves = history.map((step, move) => {
const desc = move
? `Go to move #${move} At: ${step.location}`
: "Go to game start";
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
...
}| Move location is shown |
|---|
![]() |
In this improvement we need to highlight (bold) the move that the user is currently viewing.
Following are the changes that we make in order to achieve this:
- Firstly we add a class
highlightto our css file..
.highlight {
font-weight: bold;
}- Then we add the
highlightclass dynamically to the buttons ifmove === this.state.stepNumber, which istrueonly for the button thats currently selected. This how therendermethod looks after the update.
render() {
...
const moves = history.map((step, move) => {
const desc = move
? `Go to move #${move} At: ${step.location}`
: "Go to game start";
return (
<li key={move}>
<button
className={move === this.state.stepNumber ? "highlight" : ""}
onClick={() => this.jumpTo(move)}
>
{desc}
</button>
</li>
);
});
...
}| Current move is bolded |
|---|
![]() |
In this improvement we need to make the render method of the Board component more efficient. Currently we have hardcoded the 9 squares that we need to render, which isn't quite neat, instead we can use a nested loop for this.
Following are the changes that we make in order to achieve this:
- We add a
renderBoardmethod that returns an array containing all the squares and we simply call this method inrender. Also, we need to add keys at appropriate places for React to be happy. So, our board component now looks like this.
class Board extends React.Component {
renderSquare(i) {
return (
<Square
key={i}
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
renderBoard() {
const numRows = 3;
const numCols = 3;
const board = [];
for (let r = 0; r < numRows; r++) {
let row = [];
for (let c = 0; c < numCols; c++) {
row.push(this.renderSquare(r * numCols + c));
}
board.push(
<div className="board-row" key={r}>
{row}
</div>
);
}
return board;
}
render() {
return <div>{this.renderBoard()}</div>;
}
}In this improvement we need to add a button that toggles the order in which the moves are displayed. Currently it is always displayed in ascending order (i.e from game start to the latest move), but we need to add a button to toggle this ordering from ascending to descending (i.e from latest move to game start) and visa-versa.
Following are the changes that we make in order to achieve this:
- We add a
isDescendingstate in the Game component to store the ordering of the moves. Initially in the constructor it is set tofalse. This is how our state looks now.
this.state = {
history: [
{
squares: Array(9).fill(null),
location: null,
},
],
stepNumber: 0,
xIsNext: true,
isDescending: false,
};- Next, we add a button to the
rendermethod that dynamically says "Ascending" or "Descending" based on theisDescendingstate. Also, we add a click handlertoggleOrderingto the button which we will define next. This is how our render looks after this.
render() {
...
return (
<div className="game">
...
<div className="game-info">
<div>{status}</div>
<button onClick={() => this.toggleOrdering()}>
{this.state.isDescending ? "Ascending" : "Descending"}
</button>
<ol>{moves}</ol>
</div>
</div>
);
}- Now we define the
toggleOrderingclick handler. It simply toggles theisDescendingstate.
toggleOrdering() {
this.setState({
isDescending: !this.state.isDescending,
});
}- Finally we reverse the
movesvariable in therendermethod based on theisDescendingstate. This is how our render method looks now.
render() {
...
if (this.state.isDescending) moves.reverse();
return (
...
);
}| Button to toggle the ordering of moves |
|---|
![]() |
In this improvement we need to highlight the three squares that caused the win.
Following are the changes that we make in order to achieve this:
- First we need to somehow get the positions of the winning squares, if we carefully look at how the
calculateWinnerfunction is defined we would observe that thelinesarray contains all the possible winning positions and we can easily return the winning position using this. So we now return an object that stores both the winner and wining positions. This is how the function looks now.
function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return {
winner: squares[a],
positions: lines[i],
};
}
}
return {
winner: null,
positions: [],
};
}- Since we have modified the
calculateWinnerfunction we need to make corrections in thehandleClickandrendermethod because now the function doesn't directly return the winner but it returns an object.
handleClick(i) {
...
if (calculateWinner(squares).winner || squares[i]) {
return;
}
...
}render() {
...
const winInfo = calculateWinner(current.squares);
...
if (winInfo.winner) {
status = "Winner: " + winInfo.winner;
} else {
status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}
return (
...
);
}- Next we pass a prop
winningSqsto theBoardcomponent that contains the winning positions in an array or an empty array depending on whether the game is won or not.
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
winningSqs={winData.positions}
/>- Then we pass a prop
isWinningSqto theSquarecomponent that stores whether that square is a winning square or not. So this how therenderSquaremethod looks now.
function renderSquare(i) {
return (
<Square
key={i}
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
isWinningSq={this.props.winningSqs.includes(i)}
/>
);
}- Finally lets add a class
winning-sqto our css file and add this class dynamically to the button in theSquarecomponent.
.winning-sq {
background-color: limegreen;
}function Square(props) {
return (
<button
className={`square ${props.isWinningSq ? "winning-sq" : ""}`}
onClick={props.onClick}
>
{props.value}
</button>
);
}This how the board looks like when someone wins
| Winning squares are highlighted |
|---|
![]() |
In this improvement, we need to display a message when the game is drawn.
Following are the changes that we make in order to achieve this:
- The game is drawn when all the positions in the board is filled and still we don't have a winner. So, we make the following change to incorporate this idea.
function render() {
...
let status;
if (winData.winner) {
status = "Winner: " + winData.winner;
} else {
status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}
if (!winData.winner && current.squares.every((x) => x)) status = "Draw";
return (
...
);
}| Draw message is shown |
|---|
![]() |
We have incorporated all the improvements in our game. Check out the final code in this repository.




