Geek Culture
Published in

Geek Culture

Introductory Tutorial to React: Part 2

This tutorial aims to provide a clear introduction to React (assuming no prior knowledge)

Make sure to start with Part 1 of this tutorial, click here.

Completing the Game

We now have the basic building blocks for our tic-tac-toe game. To have a complete game, we now need to alternate placing “X”s and “O”s on the board, and we need a way to determine a winner.

To check for a winner, we need to store the value of each square in one location. The best approach is to store this in the parent Board component instead of in each Square. The Board component can tell each Square what to display by passing a prop, just like we did when we passed a number to each Square.

“To collect data from multiple children, or to have two child components communicate with each other, you need to declare the shared state in their parent component instead. The parent component can pass the state back down to the children by using props; this keeps the child components in sync with each other and with the parent component.”

This might sound complicated, but you will see that a lot of the concepts, like prop passing, are all familiar from previous steps!

So the first thing we do, is add a state to the Board component. Lets call the current state squares and function updateSquares (specified later) and set the initial value to an array of 9 empty fields corresponding to the 9 squares:

const Board = () => {
const [squares, updateSquares] = useState(Array(9).fill())

Keep in mind what we did in the beginning: Inside the Board component we used <Square value={props.index}to pass the value prop (index 0 to 8) from Board to Square component as value. Let’s use the prop passing mechanism again and make value reflect the index of Board’s current state array squares:

const TicTacToeSquare = props => {
return(<Square value={squares[props.index]} />)}

Next, we need to change what happens when a Square is clicked. Remember a state hook is always private to the component that defines it, so we need to create a way for the Square to update the Board’s state. Instead, we will pass down a function from Board to the Square, and have Square call that function when a square is clicked. To pass the event handler onClick as a prop to child component, we use the function handleClick, which we specify later on:

const TicTacToeSquare = props => {
return(<Square value={squares[props.index]} onClick={() => handleClick(props.index)}/>)}

Now we’re passing down two props from Board to Square: value and onClick. So we need to make the following changes to the Square component: Delete the useState expression and const click as well as replace the state value with props.value because Square no longer keeps track of the game’s state! Instead replace click inside onClick{click} with props.onClick, which is passed down from Board:

const Square = (props) => {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
)}

Refreshing the browser at this stage will throw an error, because something in the Board component is still undefined. Try to go through it and figure out for yourself what it might be. Hint: it has to do with the useState hook below.

const [squares, updateSquares] = useState(Array(9).fill())

The answer is, we have not yet defined how the updateSquares function should update the squares state. Lets do that now, in the Board Component.

What we want to do now requires us to use the spread operator (read more about it) which allows us makes a copy of the state array or alternatively array slicing (we are not using it now, but feel free to read more about it). With that we can create a copy of squares and assign it to a new variable, whose props we can replace with “X” and which we in turn assign to the updateSquares function previously undefined in the useState hook. In the Immutability subsection we will shortly explain why we create a copy of the squares array instead of modifying the original one.

For now, implement these changes:

const Board = () => {
const handleClick = (props) => {
const newSquares = [...squares]
newSquares[props] = "X"
updateSquares(newSquares) }

What is now happening in our browser?

Actually, the same as before. When we click a square on the board it is filled with an X.

But what is happening in the background?

  1. When a user clicks on one of the squares, the onClick event handler specified in Square’s return() expression is activated.
  2. This event handler in turn calls on props.onClick(), which is the onClick prop specified in the Board component.
  3. Since the Board passes the function handleClick(props.index) to onClick, that function is called when a users clicks a square.
  4. So when that handleClick function is called, a copy is made of the state array and filled with X (note, only those squares that are clicked). This means the state of all squares is now stored in the Board component instead of the individual Square components, which allows us to determine a winner of the game in the future.

Since the Square components no longer maintain state, the Square components receive values from the Board component and inform the Board component when they’re clicked. In React terms, the Square components are now controlled components. The Board has full control over them.

Immutability

There are generally two approaches to changing data.

  1. Mutate the data by directly changing the data’s values. Example:
const player = {score: 1, name: 'Jeff'};
player.score = 2;
// Now player is {score: 2, name: 'Jeff'}
  1. Replace the data with a new copy which has the desired changes. Example:
const player = {score: 1, name: 'Jeff'};
const newPlayer = {...player, score: 2};
// using object spread syntax

Now, what are the benefits of creating a new copy?

Undo and redo: Avoiding direct data mutation lets us keep previous versions of the game’s history intact so that we can access them, for example when a user wants to undo and redo certain actions.

Detecting changes: We can easily check if the previous version is different than the changed one

Determine when a component requires re-rendering.

Now back to the Game

We now need to fix an obvious defect in our tic-tac-toe game: the “O”s cannot be marked on the board.

We’ll continue to keep the first move “X” by default. For this we create another useState hook in the Board component and set the initial state variable to true.

const [xisNext, updateXisNext] = useState(true)

Now we can use a new concept: inline if-else conditional rendering using the JavaScript conditional operator condition ? true : false. First we want to specify in the handleClick function that whenever a square is clicked the value in the newSquares array, and on the browser, will be X if xisNext is true (which it always is initially, at the start of the game). Else, if it is false, then O will appear in the clicked square. Now, how do we make xisNext false upon every second click? By using the updateXisNext function to update the state with the logical not of the current xisNext.

The exclamation mark (“!”) symbol is the logical “not” operator. Placed in front of a boolean value it reverses the value, returning the opposite. E.g., !true returns false.

const handleClick = (props) => {
const newSquares = [...squares]
newSquares[props] = xisNext? "X" : "O"
updateSquares(newSquares)
updateXisNext(!xisNext) }

Let’s also change the “status” text in Board so that it displays which player has the next turn:

const status = `Next player: ${xisNext? "X" : "O"}`

${} is used to insert a variable into a string and have it be interpreted as string

Declaring a Winner

Now we should show when the game is won and there are no more turns to make. Remember that the Squares indexes appear in this format:
0 1 2
3 4 5
6 7 8

Let’s declare a new function and call it calculateWinner with input squares. Based on the format of Squares we determine all the winning combinations and declare them as a variable (we call it lines) that we can iterate through to see if the inputed squares array has a line with all ‘X’s or all ‘O’s (i.e., win).

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],] }

So next we use a for loop and if statement to specify: For every line (defined inside the loop) of the lines array above, the array [a, b, c] is defined as a line in order to mark the column locations of the different rows when iterating through the lines array. Then if the value in location [a] in the squares array is equal to both location [b] and [c], then return the value in squares[a] location. If none of the winning line combinations is contained in input squares array, return null. As always there is more than one way to do this, and going through and playing around with alternative approaches is a very helpful exercise!

for (let line of lines) {
if (squares[a] === squares[b] && squares[a] === squares[c])
return squares[a]}
return null }

Alternatively

for (let line of lines) {
const [a, b, c] = line
const equal = squares[a] === squares[b] && squares[a] === squares[c]
return(equal? squares[a] : squares[''])}

Now, let me explain a few things in the code above:

  • We use let line of lines instead of const because the variable line is going to be reassigned.
  • If with Logical && Operator: if the condition specified before && is true, the element right after && will appear in the output. If it is false, React will ignore and skip it.
  • === (Triple equals) is a strict equality comparison operator in JavaScript, which returns false for the values which are not of a similar type.If we compare 2 with “2” using ===, then it will return a false value. Using two equal signs would return true because the string “2” is converted to the number 2 before the comparison is made.
  • The syntax equal? squares[a] : squares[‘’] denotes that if equal is true, then squares[a] is executed otherwise squares[‘’] is executed (which is an empty field)
  • You cant have JS variable declarations or loops within JSX, so instead we need to specify it as a function that is called in the return expression.

Next we call calculateWinner(squares) in the Board to check if a player has won. If a player has won, we can display text such as “Winner: X” or “Winner: O”. We’ll replace the status declaration in the Board with these lines of code:

const winner = calculateWinner(squares)
const status = winner? `Winner: ${winner}` : `Next player: ${xisNext ? 'X' : 'O'}`

We can now change the Board’s handleClick function to return early by ignoring a click if someone has einer won the game or if a Square is already filled with an X or O:

const handleClick = (props) => {
const newSquares = [...squares]
const winnerDeclared = Boolean(calculateWinner(newSquares))
const squareFilled = Boolean(newSquares[props])
if (winnerDeclared || squareFilled) return
newSquares[props] = xisNext? "X" : "O"
updateSquares(newSquares)
updateXisNext(!xisNext)}

Note: The Boolean() will return true for any non-empty, non-zero, object, or array, while it returns false if the squares are empty because no winner is yet determined by calculateWinne rin the copy of squares array.

Logical Or operator:(a || b) does not evaluate b if a is truth and returns a, but if it's false, then it evaluates and returns b.

Storing a History of Moves

As a final exercise, lets make it possible to “go back in time” to the previous moves in the game.

Remember how we used [...squares] to make a copy of the squares array after every click, instead of mutating it. This allows us to store every past version of the squares array, and navigate between the turns that have already happened. We’ll store the past squares arrays in another array called history which represents all board states, from the first to the last move. It should have the the below structure:

history = [
// Before first move
{
squares: [
null, null, null,
null, null, null,
null, null, null,
]
},
// After first move
{
squares: [
null, null, null,
null, 'X', null,
null, null, null,
]
},]

Following the same logic as the last time we lifted the state up, we want to store history state in the Board’s parent component: the Game component. This gives the Game component full control over the Board’s data, and lets it instruct the Board to render previous moves from the history.

First, we’ll set up the initial state for the Game component using a structure like the one shown above:

const Game = () => {
const initialHistory = [{ squares: Array(9).fill(null) }];
const [history, setHistory] = useState(initialHistory);
const [xIsNext, setXIsNext] = useState(true);

Next, we’ll have the Board component receive squares and onClick props from the Game component. Since we now have a single click handler in Board for many Squares, we’ll need to pass the location of each Square into the onClick handler to indicate which Square was clicked. Here are the required steps to transform the Board component:

  • Delete the useState expressions in Board.
  • Replace squares[i] with props.squares[i] in Board’s renderSquare.
  • Replace handleClick(i) with props.onClick(i) in Board’s renderSquare.
const Board = (props) => {
const TicTacToeSquare = (number) => {
return(<Square value={props.squares[number.index]}
onClick={() => props.onClick(number.index)}/>)}
return (
<div>
<div className="board-row">
<TicTacToeSquare index={0} />
...

We need to move the handleClick function from the Board component to the Game component. We also need to modify handleClickbecause the Game component’s state is structured differently. Within the Game’s handleClick function, we concatenate new history entries onto history.

const handleClick = i => {
const currentStep = history[history.length - 1];
const newSquares = [...currentStep.squares];

const winnerDeclared = Boolean(calculateWinner(newSquares));
const squareAlreadyFilled = Boolean(newSquares[i]);
if (winnerDeclared || squareAlreadyFilled) return;

newSquares[i] = xIsNext ? 'X' : 'O';
const newHistory = [...history, {squares: newSquares}];
setHistory(newHistory);
setXIsNext(!xIsNext);

What we did above looks complicated, so let me explain:

  • History provides a “screenshot” of the Board at every move, in the form of an array with the squares that are currently occupied by O or X. Then history[history.length — 1] takes only the last move from history, which contains an array with all the currently occupied squares. This is assigned to a new variable currentStep.
  • Then we use …currentStep.squares when making a copy of currentStep because we want to extract the array, which we specified in history with squares, and assign it to a new array newSquares.
  • To specify winnerDeclared we use the calculateWinner function on newSquares, to determine if there is a winner in the squares array of the last move. We make it a Boolean again to return true if there is and false if there is not (i.e., function returns null).
  • To specify squareAlreadyFilled we use newSquares[i] because whenever an empty square is clicked this returns null which is false as a boolean, while for an occupied square this returns X or O which is true when converted to a boolean.
  • if (winnerDeclared || squareAlreadyFilled) return; means that if either of those is true we return early and allow no more clicks
  • With newSquares[i] = xIsNext ? ‘X’ : ‘O’; we make sure the values alternate between X and O and are reasigned to each square in the newSquares, so that newSquares can in turn be reasigned as the squares: Array
  • Then we add newSquares back into {squares: newSquares} and adds it to a new copy of history using …history, that we call newHistory.
  • All this, to update the state history with its update function setHistory as newHistory using setHistory(newHistory)

Why history.length — 1? Because Javascript arrays are 0-based, meaning if you have an array of 5 items, you would use the indices 0 through 4 to access them. We are subtracting one to find the last index.

Then we update the Game component to use the most recent history entry to determine and display the game’s status by adding the following:

const currentStep = history[history.length - 1];
const winner = calculateWinner(currentStep.squares);
const status = winner
? `Winner: ${winner}`
: `Next player: ${xIsNext ? 'X' : 'O'}`;

Within the return expression in the Game component we update the following to reflect the changes we made above:

return (
<div className="game">
<div className="game-board">
<Board
squares={currentStep.squares}
onClick={i => handleClick(i)}

/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{}</ol>
</div>
</div>
);

At this point, the Board component only needs the renderSquare function and return expression. The game’s state and the handleClick function should be in the Game component.

Note: I initially had an error because I got mixed up in the with the calling of props from parent component Game versus the calling of internal props, index, from the element TicTacToeSquare, which represents a user-defined component. Now that we have have props like squares and onClick passed to the Board from the Game component, we need to use a seperate name (e.g., number, but this can be called anything) to pass into the user-defined TicTacToeSquare. Then we can use number.index to call on the prop index from TicTacToeSquare separately to props, which calls .squares and .onClick from the Game component.

const TicTacToeSquare = (number) => {
return(
<Square
value={props.squares[number.index]}
onClick={() => props.onClick(number.index)}/> )}

Congratulations!

You’ve created a tic-tac-toe game in your web browser and even more importantly learnt some of the basics and logic of react programming.

I will stop the tutorial here, but if you want to know how to display the history on the browser as a list of past moves use the remaining instructions from Tom Bowden.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store