Course: Section

Get Started with Modern React: Learn by Doing

Episode: Title

S03・V18: Time Travel (Part 2 – Showing the Past Moves)

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

Objectives
  1. How the array `map()` method works.
  2. Show a list of past moves in the game.
Watch Video
Duration: 6m 47s

Showing the Past Moves

Since we are recording the tic-tac-toe game’s history, we can now display it to the player as a list of past moves. To render multiple items in React, we can use an array of React elements.

JavaScript map() Method

We know that JavaScript arrays have a map() method. This method performs a specified function on each element of an array, outputting the result to a new array. Let’s show an example of the array map() method. Say we have an array of three numbers, which we will print to the console.

const numbers = [5, 2, 7];
console.log('numbers:', numbers); // Prints `numbers: [5, 2, 7]`

We can apply the map() method to the numbers array. The map() method takes a transforming function as its parameter. For example, the transforming function could be a simple function that doubles the input value, and outputs the result.

const numbers = [5, 2, 7];
console.log('numbers:', numbers); // Prints `numbers: [5, 2, 7]`
const transformFunction = x => x * 2;numbers.map(transformFunction);

With the map() method on the numbers array, the transforming function is called on each element of the array, and outputs a new array. We will assign the output array to a variable. We will print the doubled array to the console.

const numbers = [5, 2, 7];
console.log('numbers:', numbers); // Prints `numbers: [5, 2, 7]`
const transformFunction = x => x * 2;
const doubled = numbers.map(transformFunction);console.log('doubled:', doubled); // Prints `doubled: [10, 4, 14]`

You can see that we have doubled each value of the numbers array. Generally, we write the transforming function inline as an anonymous function, rather than referring to a named function. We can now delete our named transformFunction.

const numbers = [5, 2, 7];
console.log('numbers:', numbers); // Prints `numbers: [5, 2, 7]`
const doubled = numbers.map(x => x * 2);console.log('doubled:', doubled); // Prints `doubled: [10, 4, 14]`

Now, the transforming function within the map() method can actually take a second parameter. The second parameter is the index of the element within the array. Let’s give an example where we output the value of the array index in the transforming function: numbers.map((x, index) => index). We will assign the resulting array to a variable called indexes, and we will print it to console.

const numbers = [5, 2, 7];
console.log('numbers:', numbers); // Prints `numbers: [5, 2, 7]`
const doubled = numbers.map(x => x * 2);console.log('doubled:', doubled); // Prints `doubled: [10, 4, 14]`
const indexes = numbers.map((x, index) => index);
console.log('indexes:', indexes); // Prints `indexes: [0, 1, 2]`

You can see that just the index values of the numbers array are output.

Mapping over History

Using the map method, we can map our history of moves to React elements representing buttons on the screen.

We will assign the result of mapping over history to a variable called moves. We will render moves under the status text.

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

  const handleClickEvent = i => {
    const currentMove = history[history.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 = [...history, newMove];
    setHistory(newHistory);
    setXIsNext(!xIsNext);
  };

  const moves = history.map(move => {    return (      <button>        Go to move #...      </button>    );  });
  const currentMove = history[history.length - 1];
  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>
      {moves}    </div>
  );
};

Add Styling

The rendered button is too small, so let’s change its font size in index.css.

src/index.css
button {
  font-size: 32px;
}

Let’s test out the mapping of history moves to buttons in our browser. The buttons are now big enough. However, they are too close to each other. We will render the buttons in an ordered list, wrapped as list items. Then, we can give the list items some padding.

src/index.css
li {
  padding-bottom: 16px;
}

Now, let’s go back to index.js.

Wrap the button in a list item. Now, wrap the rendered moves in an ordered list.

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

  const handleClickEvent = i => {
    const currentMove = history[history.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 = [...history, newMove];
    setHistory(newHistory);
    setXIsNext(!xIsNext);
  };

  const moves = history.map(move => {
    return (
      <li>        <button>
          {description}
        </button>
      </li>    );
  });

  const currentMove = history[history.length - 1];
  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>
  );
};

Let’s see how it looks in the browser.

Showing the Move Number

Let’s show the move number in the button. We can make use of the index parameter in the map method. We will add a description string variable, showing the index of the move.

  const moves = history.map((move, index) => {
    const description = `Go to move #${index}`;    return (
      <li>
        <button>
          {description}
        </button>
      </li>
    );
  });

We can see how it looks in the browser. Instead of the first button showing Go to move #0, it would be better to display: Go to game start. Since the index number 0 becomes false when cast to a boolean, we can use a ternary expression to achieve this.

  const moves = history.map((move, index) => {
    const description = Boolean(index)      ? `Go to move #${index}`      : `Go to game start`;    return (
      <li>
        <button>
          {description}
        </button>
      </li>
    );
  });

Let’s see how it looks in the browser.

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 = 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 handleClickEvent = i => {
    const currentMove = history[history.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 = [...history, newMove];
    setHistory(newHistory);
    setXIsNext(!xIsNext);
  };

  const moves = history.map((move, index) => {
    const description = Boolean(index)
      ? `Go to move #${index}`
      : `Go to game start`;
    return (
      <li>
        <button>
          {description}
        </button>
      </li>
    );
  });

  const currentMove = history[history.length - 1];
  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;
}
src/index.css
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 explained how the array map() method works. This enabled us to show past moves in the game as buttons labeled with their index number.

Next Up…

In the next video, we will explain why it is important to add a unique key to list elements in React.