reactjs.org Tutorial

Before you read this page you need to read Linking TSX File first.
Before you read this page you need to read React > JSX and Babel > react.org Tutorial first.
The sample displayed here is exactly the same as the one on the reactjs.org/tutorial website except this one uses TypeScript interfaces.
This sample creates three React components (Square, Board and Game)
Square - This component renders a single <button>
Board - This component renders 9 Squares to create the grid
Game - This component renders the Board plus some interactive components
Lets start by creating the following two files (index.html and index.tsx)


index.htm

Create this file and embed the necessary CSS.

<!DOCTYPE html> 
<head>
<style>
body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}
ol, ul {
  padding-left: 30px;
}
.board-row:after {
  clear: both;
  content: "";
  display: table;
}
.status {
  margin-bottom: 10px;
}
.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}
.square:focus {
  outline: none;
}
.kbd-navigation .square:focus {
  background: #ddd;
}
.game {
  display: flex;
  flex-direction: row;
}
.game-info {
  margin-left: 20px;
}
</style>
</head>

<body>
<h3>BetterSolutions.com</h3>
<div id="app"></div>
</body>
</html>

index.tsx

Create this file and add the necessary interfaces for Props and State.

import * as React from 'react';    
import * as ReactDOM from 'react-dom';

interface IProps_Square {
}
interface IState_Square {
}
class Square extends React.Component <IProps_Square , IState_Square> {
  render() {
    return (
      <button className = "square">
      </button>
    )
  }
}

interface IProps_Board {
}
interface IState_Board {
}
class Board extends React.Component <IProps_Board, IState_Board> {
  renderSquare(i) {
    return <Square />;
  }
  render() {
    const status = 'Next player: X';

    return (
      <div>
        <div className = "status">{ status }</div>
        <div className = "board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className = "board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className = "board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

interface IProps_Game {
}
interface IState_Game {
}
class Game extends React.Component <IProps_Game, IState_Game> {
  render() {
    return (
      <div className = "game">
        <div className = "game-board">
          <Board />
        </div>
        <div className = "game-info">
          <div></div>
          <ol></ol>
        </div>
      </div>
    );
  }
}

ReactDOM.render(
  <Game />,
  document.getElementById('app')
);

Run the code and check that you are seeing the following in the web page.
SS


Changes 1 - Passing Data

Let's pass some data from our Board component to our Square component.
Find the IProps_Square interface and add a property called "pButtonText" that has a string data type.

interface IProps_Square { 
   pButtonText : string
}

Find the Square's render method and display the property "pButtonText" on the button.

render() { 
  return (
    <button className = "square">
      { this.props.pButtonText }
    </button>
  )
}

Find the Board's renderSquare method.
Pass in a property called "pButtonText" to the Square assigning it the value of i.

renderSquare(i) { 
  return <Square pButtonText = { i } />
}

Run the code and check that you are seeing the following in the web page.
SS


Changes 2 - Interactive Component

Let's fill the Square component with an "X" when it gets clicked.
Add an onClick event to the button that is returned from the Square's render method.
When you click on a square you will see an alert.

render() { 
  return (
    <button
       className="square"
       onClick = { () => alert('click') }
    >
      { this.props.pButtonText }
    </button>
  )
}

Run the code and check that you see a prompt when you click on any square on the board.
Find the IState_Square interface and add a state called "sButtonText" that has a string data type.

interface IState_Square { 
   sButtonText : string
}

Add a constructor to the Square class above the render method.
Initialise the "sButtonText" state inside this constructor.

constructor(props) { 
  super(props)
  this.state = {
     sButtonText : null
  }
}

Add an "onClick" event inside the Square's render method to set the state "sButtonText" to 'X'.
Change the Square's render method to display the state "sButtonText".

render() { 
  return (
    <button
       className = "square"
       onClick={ () => this.setState( { sButtonText : 'X' } ) }
    >
      { this.state.sButtonText }
    </button>
  )
}

Run the code and check.


Changes 3 - Lifting to Parent

The value of each of the 9 squares needs to be stored at the Board level which is the parent component.
Find the IState_Board interface and add a state called "sSquaresArray".

interface IState_Board { 
   sSquaresArray : Array < string >
}

Add a constructor to the Board class above the renderSquare method.
Initialise the array inside this constructor.

constructor(props) { 
  super(props);
  this.state = {
     sSquaresArray : Array(9).fill(null)
  }
}

Change the Board's renderSquare method to pass in the value from the array instead.

renderSquare(i) { 
  return <Square pButtonText = { this.state.sSquaresArray[i] } />
}

Run the code and check.


Changes 4 - Update Board State

When the Square is clicked we need to update the Board component (or state).
We cannot update the Board's state directly from Square.
Let's pass a function into Square that can be called when the Square is clicked.
Find the IProps_Square interface and add a property called "pOnClick".

interface IProps_Square { 
   pButtonText : string,
   pOnClick : any
}

Add an "onClickEvent" method to the Board class above the render method.
The slice method copies the array.

onClickEvent(i) { 
  const squares_slice = this.state.sSquaresArray.slice();
  squares_slice[i] = 'X';
  this.setState( { sSquaresArray : squares_slice } );
}

Change the Board's renderSquare method to pass in a function.
Notice we have added parenthesis to prevent JavaScript from inserting a semicolon.

renderSquare(i) { 
  return (
    <Square
      pButtonText = { this.state.sSquaresArray[i] }
      pOnClick = { () => this.onClickEvent(i) }
    />
  )
}

Change the button onClick event inside the Square's render method to call the property "pOnClick".
Change the Square's render method to display the property "pButtonText".

render() { 
  return (
    <button
       className = "square"
       onClick = { () => this.props.pOnClick() }
    >
      { this.props.pButtonText }
    </button>
  )
}

Delete the Square's constructor method.
Delete the state called sButtonText from the IState_Square interface.
Run the code and check.


Changes 5 - Taking Turns

When the squares are clicked the value needs to switch between 'X' and 'O'.
Find the IState_Board interface and add a state called "sEnterX" that has a boolean data type.

interface IState_Board { 
   sSquaresArray : Array < string >,
   sEnterX : boolean
}

Initialise the state "sEnterX" to true inside the Board's constructor .

constructor(props) { 
  super(props);
  this.state = {
     sSquaresArray : Array(9).fill(null),
     sEnterX : true
  }
}

Change the Board's "onClickEvent" method to toggle the value of the state "sEnterX".

onClickEvent(i) { 
  const squares_slice = this.state.sSquaresArray.slice();
  squares_slice[i] = this.state.sEnterX ? 'X' : 'O';
  this.setState( {
       sSquaresArray : squares_slice
       sEnterX : !this.state.sEnterX
   } );
}

Change the "status" constant inside the Board's render method to indicate which player has the next turn.

render() { 
  const status = 'Next player: ' + (this.state.sEnterX ? 'X' : 'O');
}

Run the code and check.


Changes 6 - Finding the Winner

Let's identify when there is a winner and which player it is.
Add the following function at the end of the file below the Game class.
This will check for a winner and return 'X', 'O' or null as appropriate.

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;
}

Change the "status" constant inside the Board's render method.

render() { 
  const winner = calculateWinner(this.state.sSquaresArray);
  let status
   if (winner) {
    status = 'Winner : ' + winner;
  } else {
    status = 'Next player : ' + (this.state.sEnterX ? 'X' : 'O');
  }
}

Change the Board's "onClickEvent" method to return early if someone has won the game or if the square has already been clicked.

onClickEvent(i) { 
  const squares_slice

  if (calculateWinner(squares_slice) ) { 
    return;
  }
  if (squares_slice[i]) {
    return;
  }

Run the code and check.


Changes 7 - History of Moves


Changes 8 - Showing Past Moves


Final Code

interface IProps_Game { 
}
interface IState_Game {
   history_array: any,
   xIsNext: boolean,
   stepNumber: number,
}
class Game extends React.Component < IProps_Game , IState_Game > {

  constructor(props) {
    super(props);
    this.state = {
      history_array: [{
         squares_array: Array(9).fill(null),
      }],
      stepNumber: 0, //reflects the move displayed to the user now
      xIsNext: true,
    };
  }

  handleClick(i : number) {

    const _history_array = this.state.history_array.slice(0, this.state.stepNumber + 1);
    const _current_array = _history_array[_history_array.length - 1];
    const _squares_array = _current_array.squares_array.slice();

    if (calculateWinner(_squares_array) || _squares_array[i]) {
      return;
    }

    _squares_array[i] = this.state.xIsNext ? 'X' : 'O';

    this.setState({
      history_array: _history_array.concat([{
        squares_array: _squares_array,
      }]),
      stepNumber: _history_array.length,
      xIsNext: !this.state.xIsNext,
    });
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0,
    });
  }

  render() {
    const _history_array = this.state.history_array;
    const _current_array = _history_array[this.state.stepNumber];
    const _winner = calculateWinner(_current_array.squares_array);
    const _moves = _history_array.map((step, move) => {
      const _desc = move ?
'Go to move #' + move :
'Go to game start';
      return (
        <li key={move}>
          <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_array = {_current_array.squares_array}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{ _status }</div>
          <ol>{ _moves }</ol>
        </div>
      </div>
    );
  }
}

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);

© 2020 Better Solutions Limited. All Rights Reserved. © 2020 Better Solutions Limited TopPrevNext