reactjs.org Tutorial

Before you read this page you should read React > JSX and Babel > react.org Tutorial first.
Before you read this page you should read Linking TSX File 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 modifying the following two files (index.html and index.tsx)


Upgrade to TypeScript

Before we can start making the necessary changes we need to upgrade our existing project to TypeScript.
Install the following 2 additional modules.

npm install --save-dev @babel/preset-typescript 
npm install --save-dev typescript
npm install --save-dev @types/react

Rename the "index.jsx" file to "index.tsx".
Modify the following 2 lines in the "webpack.config.js" file.

entry: __dirname + '/index.tsx',  
test: /.tsx$/,

Add another preset to the "webpack.config.js" file.

presets: ['@babel/preset-react', '@babel/preset-typescript'] 

Check the devDependencies in the "package.json" file.

"devDependencies": { 
  "@babel/core": "^7.10.3",
  "@babel/preset-react": "^7.10.1",
  "@babel/preset-typescript": "^7.1.0",
  "@types/react": "^17.0.8",
  "babel-loader": "^8.0.4",
  "react": "^16.13.1",
  "react-dom": "^16.13.1",
  "typescript": "^3.2.4",
  "webpack": "^4.43.0",
  "webpack-cli": "^3.3.12",
  "webpack-dev-server": "^3.11.0"

Execute the following two commands and check the webpage is still loading - URL - localhost:8080
SS


index.html

Replace the code in this file to include some CSS.

<!DOCTYPE html> 
<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>
   <script src="webpackbundle.js"></script>
</body>
</html>

index.tsx

Replace the code in this file to create three React components (or classes).
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')
);

Save the changes and check the web page has updated.
SS


Changes 1 - Passing Data

Let's pass some data from the Board component to the 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.
Display the property "pButtonText" on the button.

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

Find the Board's renderSquare method.
Add the property called "pButtonText" to the Square and assign it the value of i.

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

Save the changes and check the web page has updated.
SS


Changes 2 - Interactive Component

Let's display a prompt when you click on any square on the board.
Find the Square's render method.
Add an "onClick" event to the button, that displays an alert popup.

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

Run the code and check the web page.
Check that you see a prompt when you click on any square on the board.
SS
Let's fill the Square component with an "X" when we click it.
Find the IState_Square interface and add a state called "sButtonText" that has a string data type.

interface IState_Square { 
   sButtonText : string
}

Find the Square's class.
Add a constructor to the Square class (above the render method).
Initialise the state called "sButtonText" inside the constructor and initialise it with the value null.

class Square extends React.Component<IProps_Square, IState_Square> { 
  constructor(props) {
    super(props)
    this.state = {
      sButtonText : null
    }
  }

Find the Square's render method.
Change the "onClick" event inside the Square's render method to set the state "sButtonText" to 'X' (instead of displaying the alert popup).

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

Change the text that is displayed on the button inside the Square's render method to display the state "sButtonText" (instead of the property "pButtonText").

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

Save the changes and check the web page has updated.
Check that when you click on a cell an X appears.
Note: The Square class still contains the property "pButtonText" but this is not being used at the moment.
SS


Changes 3 - Lifting to Parent

Let's save the 9 square values into an array.
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" that has a string array data type.

interface IState_Board { 
   sSquaresArray : Array<string>
}

Find the Board's class.
Add a constructor to the Board class (above the renderSquare method).
Add the state called "sSquaresArray" inside this constructor and initialise it with an array of 9 null values.

class Board extends React.Component<IProps_Board, IState_Board> { 
  constructor(props) {
    super(props);
    this.state = {
      sSquaresArray : Array(9).fill(null)
    }
  }

Find the Board's renderSquare method.
Change the Square's "pButtonText" property to be the corresponding value from the "sSquaresArray" array.

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

Note: The Square's "pButtonText" property is not being used at the moment.
Save the changes and check the web page has updated.
Check the ??


Changes 4 - Update Board State

Let's save the square values into the array.
When the Square is clicked we need to update the state of the Board.
We cannot update the Board's state directly from Square.
Find the Square's class.
Find the IProps_Square interface and add a property called "pOnClick".

interface IProps_Square { 
   pButtonText : string,
   pOnClick : any
}

Find the Board's class.
Add a method called "onClickEvent" to the Board's class (above the render method).
This method changes the corresponding value in the "sSquaresArray" array and reassigns a new array to this state.
The slice method copies the array.

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

Find the Board's renderSquare method.
Change the return statement to use parentheses (so we can write it over multiple lines).

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

Add the property called "pOnClick" to the Square, assigning it to the onClickEvent method.

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

Find the Square's render method.
Change the "onClick" event of the button to call the property "pOnClick" (instead of setting the state "sButtonText").

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

Change the text that is displayed on the button to display the property "pButtenText" (instead of the state "sButtonText").

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

Find the IState_Square interface and remove the state called "sButtonText".

interface IState_Square { 
}

Find the Square's constructor method.
Remove the state "sButtonText" from the Square's constructor (because this state has been moved to a property).
Remove the Square's constructor method.

class Square extends React.Component<IProps_Square, IState_Square> { 
  // constructor(props) { } - remove this whole method
  render() {
    return (

Save the changes and check the web page has updated.
Check the ??


Changes 5 - Taking Turns

Let's change the message at the top to indicate which player has the next turn.
When the squares are clicked the value needs to switch between 'X' and 'O'.
Find the Board's class
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
}

Find the Board's constructor.
Add a state called "sEnterX" inside the constructor and initialise it to true.

class Board extends React.Component<IProps_Board, IState_Board> { 
  constructor(props) {
    super(props);
    this.state = {
      sSquaresArray : Array(9).fill(null),
      sEnterX : true
    }
  }

Find the Board's onClickEvent method.
The slice method copies the array.
Change the corresponding value in the "squares_slice" array to read the state called "sEnterX" and toggle between 'X' and 'O'.

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

Inside the setState, toggle the value of the state called "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
  } );
}

Find the Board's render method.
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');
}

Save the changes and check the web page has updated.
Check the ??
SS


Changes 6 - Finding the Winner

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

function calculateWinner(squaresArray : Array<string>) { 
  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 (squaresArray[a] && squaresArray[a] === squaresArray[b] && squaresArray[a] === squaresArray[c]) {
      return squaresArray[a];
    }
  }
  return null;
}

Find the Board's render method.
Change the "status" constant inside the Board's render method.
Replace the "const status" line with these seven lines.

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

  return (
}

Find the Board's onClickEvent method.
Change the method to return early if someone has won the game or if the square has already been clicked.
Replace the "const squares_slice" line with these seven lines.

onClickEvent(i) { 
  const squares_slice = this.state.sSquaresArray.slice();
  if (calculateWinner(squares_slice) ) {
    return;
  }
  if (squares_slice[i]) {
    return;
  }

  squares_slice[i] = this.state.sEnterX ? 'X' : 'O';
}

Run the code and check the web page.
Check the ??
SS


Changes 7 - History of Moves

Let's keep a record of all the moves.
To do this we create another array called "sHistoryArray" and in this array we save a copy of the "sSquaresArray" after each turn.
The history array represents all the board states, from the first move to the last move.
We want the Game component to be able to access the history array.
For this we need to add a state called "sHistoryArray" to the Game class.
Using a state lets you pass information from the child to the parent.
Using a property lets you pass information from the parent to the child.


Find the IState_Game interface.
Add a state called "sHistoryArray" that has an array of an object that contains a string array data type.
Add a state called "sEnterX" that has a boolean data type.

interface IState_Game { 
  sHistoryArray : Array< { squaresArray: Array<string> }>,
  sEnterX : boolean
}

Find the Game class.
Add a constructor to the Game class (above the render method).
This constructor defines the starting point for a new game.
Add a state called "sHistoryArray" inside this constructor and initialise it with an array of 9 null values.
Add a state called "sEnterX" inside the constructor and initialise it to true.

class Game extends React.Component<IProps_Game, IState_Game> { 
  constructor(props) {
    super(props);
    this.state = {
      sHistoryArray: [{
        squaresArray : Array(9).fill(null),
      }],
      sEnterX : true,
    };
  }

At the moment the Board class contains the "sSquaresArray" state.
The "sSquaresArray" array needs to be accessible from the Board.
To achieve this we lift the state to the parent and change it to a property.
We can replace the Board's "sSquaresArray" state with a property instead which means this value can be passed down from the Game class to the Board class.
Find the IState_Board interface.
Remove the state called "sSquaresArray".
Remove the state called "sEnterX".

interface IState_Board { 
}

Find the Board's constructor.
Remove the state "sSquaresArray" from the Board's constructor (because this now being passed down as a property).
Remove the state "sEnterX" from the Board's constructor (because this state has been moved to the Game class).
Remove the Board's constructor method.

class Board extends React.Component<IProps_Board, IState_Board> { 
  // constructor(props) { } - remove this whole method
  renderSquare(i) {
    return (

Find the IProps_Board interface.
Add a property called "pSquaresArray" that has a string array data type.
Add a property called "pOnClick" that has a ?? data type.

interface IProps_Board { 
  pSquaresArray : Array<string>,
  pOnClick : any
}

Find the Board's renderSquare method.
Change the Square's "pButtonText" property to be a value from the Board's "pSquaresArray" property (instead of the state sSquaresArray).
Change the Square's "pOnClick" property to call the Board's "pOnClick" property (instead of the method)

renderSquare(i) { 
  return (
    <Square
      pButtonText = { this.props.pSquaresArray[i] }
      pOnClick={ () => this.props.pOnClick(i) }
    />
  )
}

Move the seven lines of code below the Board's render method to below the Game'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');
  }

Add a line of code to initialise a history array.
Add a line of code to initialise a current array.
Change the argument passed to the calculateWinner method to be "current.squares" (instead of "this.state.squares")

render() { 
  const history = this.state.sHistoryArray;
  const current = history[history.length - 1];
  const winner = calculateWinner(current.squaresArray);

  let status;

Find the Game's render method.
Add the property called "pSquaresArray" to the Board.
Add the property called "pOnClick" to the Board that calls the "OnClickEvent" method.

render() { 
  return (
    <div className="game">
      <div className="game-board">
        <Board
           pSquaresArray = { current.squaresArray }
           pOnClick = { (i) => this.onClickEvent(i) }
        />

Find the Board's render method.
The status of the game needs to be rendered from the Game class (and not the Board's class).
Remove the status from the Board's return method.

return ( 
  <div>
    <div className="board-row">
      {this.renderSquare(0)}
      {this.renderSquare(1)}
      {this.renderSquare(2)}
    </div>

Find the Game's render method.
Add the status to the Game's return method.

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

Find the Board's onClickEvent method.
At the moment the Board's class contains a onClickEvent method.
Move the "onClickEvent" method from the Board's class to the Game's class (above the render method).

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

Add a line of code to initialise a history array.
Add a line of code to initialise a current array.
Change the line of code that initialises the squares_slice array.
Inside the setState, change the state called "sSquaresArray" to "sHistoryArray" so it is appended to the history.
The concat method does not mutate the original array.

onClickEvent(i) { 
  const history = this.state.sHistoryArray;
  const current = history[history.length - 1];
  const squares_slice = current.squaresArray.slice();

  if (calculateWinner(squares_slice) ) {
    return;
  }
  if (squares_slice[i]) {
    return;
  }
  squares_slice[i] = this.state.sEnterX ? 'X' : 'O';
  this.setState({
    sHistoryArray : history.concat([{
      squaresArray : squares_slice,
    }]),
    sEnterX : !this.state.sEnterX,
  });
}

Run the code and check the web page.
Check the ??
SS


Changes 8 - Showing The Past Moves

Now that we are storing a history of the moves we can display this to to the user.
Using the JavaScript array's map method, we can map our history of moves to React elements.
We can display this as buttons on the screen, and display a list of buttons to allow the user to "jump" to past moves.


Find the Game's render method.
Add a variable called moves that can return a list item containing a button element.

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

Find the Game's render method
Add the moves to the Game's return method.

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

We need to specify a key property for each list item to differentiate each list item from its siblings.
"key" is a special and reserved property in React
React automatically uses key to decide which components to update.
It's strongly recommended that you assign proper keys whenever you build dynamic lists.
Keys do not need to be globally unique; they only need to be unique between components and their siblings.
Each past move has a unique ID associated with it: it's the sequential number of the move.
The moves are never re-ordered, deleted, or inserted in the middle, so it's safe to use the move index as a key.


In the Game component's render method, we can add the key as <li key={move}>

      return ( 
        <li key={move}>
        <button
          onClick={() => this.jumpTo(move)}>{desc}
        </button>
        </li>
      );

Add stepNumber to the Game component's state to indicate which step we're currently viewing.
Find the Game's constructor.

class Game extends React.Component { 
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      stepNumber: 0,
      xIsNext: true,
    };
  }

Add the jumpTo method

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

The stepNumber state we've added reflects the move displayed to the user now.
After we make a new move, we need to update stepNumber by adding stepNumber: history.length as part of the this.setState argument.
This ensures we don't get stuck showing the same move after a new one has been made.


Change the handleClick method.
Change the history constant.
Change the stepNumber to be initialised to history.length

handleClick(i) { 
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares2 = current.squares.slice();

    if (calculateWinner(squares2) || squares2[i]) {
      return;
    }
    squares2[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares2
      }]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext,
    });
  }

FInd the Game's render method
Change the current constant

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



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