Course: Section

Get Started with Modern React: Learn by Doing

Episode: Title

S03・V16: Return Early

Date Created: July 17th, 2019
Last Updated: 27 days ago

Objectives
  1. We will ignore clicks on the board when a player has already won.
  2. We will ignore clicks on squares that already have been filled.
Watch Video
Duration: 4m 7s

Return Early

We need to deal with players clicking on the board when someone has already won the game, or if a square is already filled.

Let’s demonstrate these situations in the browser. We will create a situation where player X wins the game.

  1. Player X clicks on top-left square (index 0)
  2. Player O clicks on top-center square (index 1)
  3. Player X clicks on center square (index 4)
  4. Player O clicks on top-right square (index 2)
  5. Player X clicks on bottom-right square (index 8)

Player X is the winner.

Now if we click again, say in bottom-left square (index 6), we see that an O appears, which we want to avoid occurring. If we click on the same square again, we see that the square filled with an O, changes to an X, which we also want to avoid.

We will make changes to the handleClickEvent function to fix these issues. Let’s add a comment.

  const handleClickEvent = i => {
    const newSquares = [...squares];

    // Return early if winner declared or square filled
    newSquares[i] = xIsNext ? 'X' : 'O';
    setSquares(newSquares);
    setXIsNext(!xIsNext);
  };

To check if a player has already won, we can use our calculateWinner helper function: calculateWinner(newSquares). This will return either an X or an O if there is already a winner, or null if there is not a winner yet. So, we can cast it to a boolean, which will be true if there is a winner, and false if there is not a winner: Boolean(calculateWinner(newSquares)). We will assign this boolean to a variable called winnerDeclared.

  const handleClickEvent = i => {
    const newSquares = [...squares];

    // Return early if winner declared or square filled
    const winnerDeclared = Boolean(calculateWinner(newSquares));
    newSquares[i] = xIsNext ? 'X' : 'O';
    setSquares(newSquares);
    setXIsNext(!xIsNext);
  };

Now, let’s check if a square has already been filled: newSquares[i]. This will evaluate to either X or O if already filled, and null if not filled. So, if we cast it to a boolean, it will be true if filled, and false if not filled: Boolean(newSquares[i]). We will assign this boolean to a variable called squareFilled.

  const handleClickEvent = i => {
    const newSquares = [...squares];

    // Return early if winner declared or square filled
    const winnerDeclared = Boolean(calculateWinner(newSquares));
    const squareFilled = Boolean(newSquares[i]);
    newSquares[i] = xIsNext ? 'X' : 'O';
    setSquares(newSquares);
    setXIsNext(!xIsNext);
  };

Now, we can perform a check to see if either of these conditions is true: winnerDeclared || squareFilled. If either condition is true, we should return early from the handleClickEvent function, doing nothing.

  const handleClickEvent = i => {
    const newSquares = [...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';
    setSquares(newSquares);
    setXIsNext(!xIsNext);
  };

Now, let’s test this out in the browser. As before, we will create a situation where player X wins the game. Player X is the winner. Now if we click again, nothing appears, which is what we want. Now if we click on a square which is already filled, such as the X in the bottom right square, nothing happens, which is what we want.

Congratulations!

You now have a working tic-tac-toe game. And you’ve just learned the basics of React too. So, you’re probably the real winner here!

Code Snapshot

src/index.js
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 = () => {
  const initialSquares = Array(9).fill(null);
  const [squares, setSquares] = useState(initialSquares);
  const [xIsNext, setXIsNext] = useState(true);

  const handleClickEvent = i => {
    const newSquares = [...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';
    setSquares(newSquares);
    setXIsNext(!xIsNext);
  };

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

  const renderSquare = i => {
    return (
      <Square
        value={squares[i]}
        onClickEvent={() => handleClickEvent(i)}
      />
    );
  };

  return (
    <div>
      <div className="status">{status}</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 = () => {
  return (
    <div className="game">
      <Board />
    </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;
}

Summary

We updated the handleClickEvent function, so that it ignored clicks when a player had already won. And, we also made sure the game ignored clicking on squares that had already been filled.

Next Up…

In the next video, we will be adding time travel to our game.