Get Started with Modern React: Learn by Doing
S03・V20: Time Travel (Part 3 – Implementing Time Travel)
- We will implement the ability to jump to any previous move in the game’s history, and also to allow for a new sequence of moves from that point forward.
Implementing Time Travel
We will now implement the final feature of our game. When clicking on a button specifying a certain move, the game will jump to that move in the game’s history. This will complete our time travel feature.
First, we will remember the index of the move that we want to jump to, in our Game
’s state.
We will call the state variable moveIndex
, and we will initialize it with 0
.
const Game = () => {
const initialHistory = [
{ squares: Array(9).fill(null) },
];
const [history, setHistory] = useState(initialHistory);
const [xIsNext, setXIsNext] = useState(true);
const [moveIndex, setMoveIndex] = useState(0);
Jump to Move
Next, we will create a function that will be called when the user clicks on a move button, to jump to the move specified by the move’s index. Inside this function, we will set the move index to be the index
of the move button: setMoveIndex(index)
.
In order to check if player X
or player O
is next, we first have to ascertain if the move index is even or odd. We will use the modulus operator to get the remainder after dividing the index
by 2
. If the remainder is 0
, then the index is even, otherwise it is odd: const moveIndexIsEven = index % 2 === 0
.
Player X
is next if the move index is even: setXIsNext(moveIndexIsEven)
.
const jumpTo = index => {
setMoveIndex(index);
const moveIndexIsEven = index % 2 === 0;
setXIsNext(moveIndexIsEven);
};
We will now pass this jumpTo(index)
function to the move button’s onClick
event.
const moves = history.map((move, index) => {
const description = Boolean(index)
? `Go to move #${index}`
: `Go to game start`;
return (
<li key={index}>
<button onClick={() => jumpTo(index)}> {description}
</button>
</li>
);
});
Update ‘handleClickEvent’
We will now make a few changes to the handleClickEvent
function which fires when you click on a square. First off, we need to make a copy of history
, and slice off the moves past the selected move index, so that fresh moves can be made by the game players after that point in the game: history.slice(0, moveIndex + 1)
. We will assign this copy of history
to a variable called slicedHistory
.
Now, we will need to update the following references to history
to read slicedHistory
. This ensures that if we “go back in time” and then make a new move from that point, we throw away all the “future” history that would now become incorrect.
The moveIndex
state variable we’ve added reflects the move displayed to the user now. After we make a new move, we need to update the moveIndex
with the setMoveIndex
function, passing in the history up to and including the move selected by the user: setMoveIndex(slicedHistory.length)
. This ensures we don’t get stuck showing the same move after a new one has been made.
const handleClickEvent = i => {
const slicedHistory = history.slice(0, moveIndex + 1); const currentMove = slicedHistory[slicedHistory.length - 1]; const newSquares = [...currentMove.squares];
// Return early if winner declared or square filled
const winnerDeclared = Boolean(calculateWinner(newSquares));
const squareFilled = Boolean(newSquares[i]);
if (winnerDeclared || squareFilled) {
return;
}
newSquares[i] = xIsNext ? 'X' : 'O';
const newMove = { squares: newSquares };
const newHistory = [...slicedHistory, newMove]; setHistory(newHistory);
setXIsNext(!xIsNext);
setMoveIndex(slicedHistory.length); };
Finally, we will modify the Game
component from always rendering the last move, to rendering the currently selected move, according to the moveIndex
.
const currentMove = history[moveIndex]; const winner = calculateWinner(currentMove.squares);
const status = winner ?
`Winner: ${winner}` :
`Next player: ${xIsNext ? 'X' : 'O'}`;
Now the time travel feature is complete!
Let’s try it out in the browser.
- Player
X
starts in the top left square. - Player
O
follows with a move in the square below. - Player
X
’s second move is in the top right square. - Player
O
’s second move is in the top middle square. - Player
X
then moves to the bottom left square. - Player
O
blocks with a move in the center square. - Player
X
follows with a move to the bottom middle square. - And, player
O
wins with a move to the right central square.
Now, with time travel, the game allows us to jump to a previous move. For example, we could Go to move #1
.
Or, we could Go to move #4
, and play new moves from there.
- Player
X
is next, and this time around, moves to the center square. - Note that move
#5
is now different from before, and all moves beyond#5
have been “sliced off”. - Now, next up is player
O
, who moves to the bottom right square. - And, this time, player
X
wins with a move to the bottom left square.
We have now verified that the game works as we intended it to.
Game Features
Congratulations! You’ve created a tic-tac-toe game that:
- Lets you play tic-tac-toe,
- Indicates when a player has won the game,
- Stores a game’s history as a game progresses,
- And, allows players to review a game’s history and see previous versions of a game’s board.
Nice work! We hope you now feel like you have a decent grasp on how React works.
Code Snapshot
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import './index.css';
const Square = props => {
return (
<button
className="square"
onClick={props.onClickEvent}
>
{props.value}
</button>
);
};
const Board = props => {
const renderSquare = i => {
return (
<Square
value={squares[i]}
onClickEvent={() => handleClickEvent(i)}
/>
);
};
return (
<div>
<div className="board-row">
{renderSquare(0)}
{renderSquare(1)}
{renderSquare(2)}
</div>
<div className="board-row">
{renderSquare(3)}
{renderSquare(4)}
{renderSquare(5)}
</div>
<div className="board-row">
{renderSquare(6)}
{renderSquare(7)}
{renderSquare(8)}
</div>
</div>
);
};
const Game = () => {
const initialHistory = [
{ squares: Array(9).fill(null) },
];
const [history, setHistory] = useState(initialHistory);
const [xIsNext, setXIsNext] = useState(true);
const [moveIndex, setMoveIndex] = useState(0);
const handleClickEvent = i => {
const slicedHistory = history.slice(0, moveIndex + 1);
const currentMove = slicedHistory[slicedHistory.length - 1];
const newSquares = [...currentMove.squares];
// Return early if winner declared or square filled
const winnerDeclared = Boolean(calculateWinner(newSquares));
const squareFilled = Boolean(newSquares[i]);
if (winnerDeclared || squareFilled) {
return;
}
newSquares[i] = xIsNext ? 'X' : 'O';
const newMove = { squares: newSquares };
const newHistory = [...slicedHistory, newMove];
setHistory(newHistory);
setXIsNext(!xIsNext);
setMoveIndex(slicedHistory.length);
};
const jumpTo = index => {
setMoveIndex(index);
const moveIndexIsEven = index % 2 === 0;
setXIsNext(moveIndexIsEven);
};
const moves = history.map((move, index) => {
const description = Boolean(index)
? `Go to move #${index}`
: `Go to game start`;
return (
<li key={index}>
<button onClick={() => jumpTo(index)}>
{description}
</button>
</li>
);
});
const currentMove = history[moveIndex];
const winner = calculateWinner(currentMove.squares);
const status = winner ?
`Winner: ${winner}` :
`Next player: ${xIsNext ? 'X' : 'O'}`;
return (
<div className="game">
<Board
squares={currentMove.squares}
onClickEvent={i => handleClickEvent(i)}
/>
<div className="status">{status}</div>
<ol>{moves}</ol>
</div>
);
};
ReactDOM.render(
<Game />,
document.getElementById('root')
);
function calculateWinner(squares) {
/*
0 1 2
3 4 5
6 7 8
*/
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 line of lines) {
const [a, b, c] = line;
if (
squares[a] &&
squares[a] === squares[b] &&
squares[a] === squares[c]
) {
return squares[a];
}
}
return null;
}
body {
background-color: #444;
color: white;
margin: 20px;
font: 32px "Century Gothic", Futura, sans-serif;
}
button {
font-size: 32px;
}
li {
padding-bottom: 16px;
}
.game {
display: flex;
flex-direction: column;
align-items: center;
}
.board-row {
display: flex;
flex-flow: row nowrap;
}
.square {
background-color: #444;
color: white;
border: 1px solid #999;
padding: 0;
font-size: 84px;
text-align: center;
width: 100px;
height: 100px;
}
.square:focus {
outline: none;
background-color: #222;
}
.status {
margin: 20px;
}
Summary
We implemented the ability to jump to any previous move in the game’s history, which allowed players to make new moves from that point forward. This was the final explanatory video for the React game tutorial.
Next Up…
In the next video, we will issue some challenge questions that cover this section.