/tic-tac-toe

Build Tic-Tac-Toe with ReactJS from scratch (includes time travel to check history of every move).

Primary LanguageJavaScript

tic-tac-toe

Build an interactive tic-tac-toe game using React from scratch - coding assessment.

  1. npx create-react-app tic-tac-toe (make sure node is up-to-date!)

  2. Delete original source files inside 'src' folder (cd tic-tac-toe, cd src, rm -f *, cd..)

  3. Create 'index.css', 'index.js' inside 'src' folder.

  4. In Board’s renderSquare method, change the code to pass a prop called value to the Square:

    class Board extends React.Component {
        renderSquare(i) {
            return <Square value={i} />;
        }
    }
    
  5. Change Square’s render method to show that value by replacing {/_ TODO _/} with {this.props.value}:

    class Square extends React.Component {
        render() {
            return (
            <button className="square">
                {this.props.value}
            </button>
            );
        }
    }
    
  6. Change the button tag that is returned from the Square component’s render() function to this:

    class Square extends React.Component {
        render() {
            return (
            <button className="square" onClick={function() { alert('click'); }}>
                {this.props.value}
            </button>
            );
        }
    }
    
  7. Store the current value of the Square in this.state, and change it when the Square is clicked.

First, we’ll add a constructor to the class to initialize the state:

    class Square extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
            value: null,
            };
    }
  1. Now we’ll change the Square’s render method to display the current state’s value when clicked:

Replace this.props.value with this.state.value inside the tag. Replace the onClick={...} event handler with onClick={() => this.setState({value: 'X'})}. Put the className and onClick props on separate lines for better readability. After these changes, the tag that is returned by the Square’s render method looks like this:

    class Square extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
            value: null,
            };
        }

        render() {
            return (
            <button
                className="square"
                onClick={() => this.setState({value: 'X'})}
            >
                {this.state.value}
            </button>
            );
        }
    }
  1. Add a constructor to the Board and set the Board’s initial state to contain an array of 9 nulls corresponding to the 9 squares:

    class Board extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
            squares: Array(9).fill(null),
            };
    }
    
  2. Modify the Board’s renderSquare method to read from it:

    renderSquare(i) {
        return <Square value={this.state.squares[i]} />;
    }
    
  3. Pass down a function from the Board to the Square, and we’ll have Square call that function when a square is clicked. We’ll change the renderSquare method in Board to:

    renderSquare(i) {
        return (
        <Square
            value={this.state.squares[i]}
            onClick={() => this.handleClick(i)}
        />
        );
    }
    
  4. Make the following changes to Square:

  • Replace this.state.value with this.props.value in Square’s render method
  • Replace this.setState() with this.props.onClick() in Square’s render method
  • Delete the constructor from Square because Square no longer keeps track of the game’s state

After these changes, the Square component looks like this:

    class Square extends React.Component {
        render() {
            return (
            <button
                className="square"
                onClick={() => this.props.onClick()}
            >
                {this.props.value}
            </button>
            );
        }
    }
  1. Add handleClick to the Board class:

    class Board extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
            squares: Array(9).fill(null),
            };
    }
    
    handleClick(i) {
        const squares = this.state.squares.slice();
        squares[i] = 'X';
        this.setState({squares: squares});
    }
    
  2. Replace the Square class with this function:

    function Square(props) {
        return (
            <button className="square" onClick={props.onClick}>
            {props.value}
            </button>
        );
    }
    
  3. Set the first move to be “X” by default. We can set this default by modifying the initial state in our Board constructor:

    class Board extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
            squares: Array(9).fill(null),
            xIsNext: true,
            };
    }
    
  4. xIsNext (a boolean) will be flipped to determine which player goes next and the game’s state will be saved. We’ll update the Board’s handleClick function to flip the value of xIsNext:

    handleClick(i) {
        const squares = this.state.squares.slice();
        squares[i] = this.state.xIsNext ? 'X' : 'O';
        this.setState({
            squares: squares,
            xIsNext: !this.state.xIsNext,
        });
    }
    
  5. Change the “status” text in Board’s render so that it displays which player has the next turn:

        render() {
            const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    
  6. Copy this helper function and paste it at the end of the file:

    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 squares[a];
            }
        }
        return null;
    }
    
  7. We will call calculateWinner(squares) in the Board’s render function 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 Board’s render function with this code:

        render() {
                const winner = calculateWinner(this.state.squares);
                let status;
                if (winner) {
                status = 'Winner: ' + winner;
                } else {
                status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
            }
    
  8. We can now change the Board’s handleClick function to return early by ignoring a click if someone has won the game or if a Square is already filled:

           handleClick(i) {
            const squares = this.state.squares.slice();
            if (calculateWinner(squares) || squares[i]) {
                return;
            }
    
  9. We’ll set up the initial state for the Game component within its constructor:

        class Game extends React.Component {
            constructor(props) {
                super(props);
                this.state = {
                history: [{
                    squares: Array(9).fill(null),
                }],
                xIsNext: true,
            };
        }
    
  10. 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:

  • Delete the constructor in Board.
  • Replace this.state.squares[i] with this.props.squares[i] in Board’s renderSquare.
  • Replace this.handleClick(i) with this.props.onClick(i) in Board’s renderSquare.
  1. Update the Game component’s render function to use the most recent history entry to determine and display the game’s status:

    render() { const history = this.state.history; const current = history[history.length - 1]; const winner = calculateWinner(current.squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); }

       return (
       <div className="game">
           <div className="game-board">
           <Board
               squares={current.squares}
               onClick={(i) => this.handleClick(i)}
           />
           </div>
           <div className="game-info">
               <div>{status}</div>
    
  2. The Game component is now rendering the game’s status, we can remove the corresponding code from the Board’s render method. After refactoring, the Board’s render function looks like this:

    render() {
        return (
        <div>
            <div className="board-row">
    
  3. We need to move the handleClick method from the Board component to the Game component. We also need to modify handleClick because the Game component’s state is structured differently. Within the Game’s handleClick method, we concatenate new history entries onto history.

    handleClick(i) {
        const history = this.state.history;
        const current = history[history.length - 1];
        const squares = current.squares.slice();
        if (calculateWinner(squares) || squares[i]) {
            return;
        }
        squares[i] = this.state.xIsNext ? 'X' : 'O';
        this.setState({
        history: history.concat([{
            squares: squares,
        }]),
        xIsNext: !this.state.xIsNext,
        });
    }
    
  4. Map over the history in the Game’s render method:

    render() {
        const history = this.state.history;
        const current = history[history.length - 1];
        const winner = calculateWinner(current.squares);
    
        const moves = history.map((step, move) => {
        const desc = move ?
            'Go to move #' + move :
            'Go to game start';
        return (
            <li>
            <button onClick={() => this.jumpTo(move)}>{desc}</button>
            </li>
        );
        });
    
        let status;
        if (winner) {
        status = 'Winner: ' + winner;
        } else {
        status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
        }
    
        return (
        <div className="game">
            <div className="game-board">
            <Board
                squares={current.squares}
                onClick={(i) => this.handleClick(i)}
            />
            </div>
            <div className="game-info">
            <div>{status}</div>
            <ol>{moves}</ol>
    
  5. In the Game component’s render method, we can add the key as

  6. and React’s warning about keys should disappear:

    const moves = history.map((step, move) => {
        const desc = move ?
            'Go to move #' + move :
            'Go to game start';
        return (
            <li key={move}>
    
  7. Add stepNumber: 0 to the initial state in Game’s constructor:

    class Game extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
            history: [{
                squares: Array(9).fill(null),
            }],
            stepNumber: 0,
    
  8. Next, we’ll define the jumpTo method in Game to update that stepNumber. We also set xIsNext to true if the number that we’re changing stepNumber to is even:

    handleClick(i) {
        // this method has not changed
    }
    
    jumpTo(step) {
        this.setState({
        stepNumber: step,
        xIsNext: (step % 2) === 0,
        });
    }
    
  9. Replace reading this.state.history with this.state.history.slice(0, this.state.stepNumber + 1). 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:

    handleClick(i) {
        const history = this.state.history.slice(0, this.state.stepNumber + 1);
        const current = history[history.length - 1];
        const squares = current.squares.slice();
        if (calculateWinner(squares) || squares[i]) {
            return;
        }
        squares[i] = this.state.xIsNext ? 'X' : 'O';
        this.setState({
            history: history.concat([{
                squares: squares
            }]),
            stepNumber: history.length,
    
  10. Modify the Game component’s render method from always rendering the last move to rendering the currently selected move according to stepNumber:

    render() {
        const history = this.state.history;
        const current = history[this.state.stepNumber];
    

Thanks to Official React Page for this!