React toy project: Tactics (part 2)

In part 1 of this series I introduced the dartgame Tactics I am going to build:

In Tactics, two players hit the numbers 20 to 10 three times each. Once a number is completed (i.e., you hit it three times) but your opponent did not, you can score points by hitting the number again. When both players have all numbers completed the points are calculated and the player with the highest score wins.

react-tactics-static-version

The static version was created, but without any interaction it ain’t a game yet.

Working prototype

So now it is time to add some interaction. The easiest part here is the changeTurn button, the red button in the middle. Clicking it should simply change turns, i.e., make the other player’s field active. After that, hitting a number on the dartboard should be as easy as clicking a number on the screen. The number should be crossed out and if a row is completed with that hit, it should be marked as complete. Furthermore, it should be possible to score points when you have a number complete while your opponent hasn’t yet. Lastly, at the end of the game the standings should be updated and it should be possible to play again.

Defining state

The user interface you see in a React app is based on the state of the app. In short, the state contains the data for the app. In a traditional MVC framework state could be compared to the data inside models. But also UI state (such as ‘which player is on turn’) is in the state of a React app. In our Tactics game I have defined the following parts of the state:

  • startTurn (boolean): defines which player is starting the game
  • gameTurn (boolean): defines which player is on turn
  • gameFinished (boolean): defines whether the game is finished
  • player1 (object): contains information for player1, including the player name, the number of rounds won, whether it is his turn and lastly, an object containing the score
  • player2 (object): idem for the other player

Note that it seems ambiguous to have both a gameTurn and a player.turn in state. However, when working on the app I found it quite usefull. Still it is redundant code which should be avoided and I’ll probably remove one or the other in a future version.

Whenever the state changes, the app is rerendered automatically.

Changing state

It is best practise in React to have only one, or a few, stateful components. Stateful components are those that have a state and contain functions to change it. In our Tactics game, the main (or root) component (<Tactics />) will contain the state. All other components will get their data via props and are in fact not stateful components. They just render with the data that is passed to them.

A little more on props and passing them around

In the post on the static version of the Tactics app we defined the components:

  • Tactics [main component]
    • Header
    • TacticsGame
      • PlayerTurner
      • Standings
      • PlayerField
        • Row
          • PointsCounter

Our state (a simple JS object) lives inside the <Tactics /> root component and is used inside this component as this.state. The <Tactics /> component (as each component in React) contains a render method which renders the app and a bunch of functions to change the state. In our case, the render function just renders a wrapper div with inside it the <Header /> component and the <TacticsGame /> component. Each of these components have a render function that either returns some ‘normal’ HTML or other components.

As you can see, the <TacticsGame /> component renders <PlayerTurner />, <Standings /> and <PlayerField /> components and the <PlayerField /> component renders <Row components /> which in turn each render a <PointsCounter /> component.

Some of these components simply render static HTML, others need some dynamic data or access to the functions inside the root component. It’s these data and functions that should be passed via props from the root component (<Tactics />) all the way down to the component that needs the data.

As an example, a <Row /> component corresponds to a specific number to hit, say 17. Assume as well that you have hit it twice so far. To render the row correctly, the <Row /> component needs this information. Hence, it is passed down all the way from the root component via the intermediate components to the <Row /> component. Inside the row component, the number is for example retrieved using this.props.number.

Look a the code in Bitbucket to get this point!

As an example, let’s look how to implement clicking the <PlayerTurner /> button to change turns. How does clicking the button change state?

PlayerTurner component in detail

Below you see the component code:

The PlayerTurner component is actually no more than a button. It is included in the render function of the component and it is passed one prop ‘changeTurn’:

To enable the clicking of the PlayerTurner button, an onClick attribute is set ({this.props.changeTurn}). This means that clicking the button will execute the changeTurn function that was passed down as a prop from the parent component (). If we walk up the tree to the root component (), we see that it contains the changeTurn function which it passes down to the components that need access to it.

The changeTurn function above works as follows: when the button is clicked, we need to change some booleans in the state and call the setState function to update the state and let the app rerender itself.

More interactivity

A more interesting example is the hitNumber function inside the <Tactics /> component (available in the Bitbucket repo). It is passed downwards from the root component all the way to the Row component and triggerred when the user clicks a number. This function accepts two arguments, the number that was hit and a boolean turn (only the player on turn may hit numbers!).

Note that since we use the function as a callback in the onClick attribute, we need to bind the arguments using bind. Note that the first argument to bind is the reference to this which in this case is just null. The hitNumber function itself will update state and rerenders the app.

When a row is completed by one player and not yet by the other player, a button appears at the end of the row. Clicking the button triggers the function addPoints that was also passed down. Its code is below:

Finally there is one more function which is triggered at the end of a game. It is the resetGame function and it simply resets the scores and the turn booleans and results in a fresh Tactics game field.

A word on performance

Until this point we haven’t bothered about performance at all. All we did was creating a simple app to play the Tactics game. However, playing it in a bar it quickly turns out that the UI does not respond as fast as one would like. This is due to too many rerenders since the whole app is rerendered even if only one number should be crossed after a hit.

But it is not the fault of the framework!

It’s simply because we did not take any notice of things like immutability of state, pure render functions or the shouldComponentUpdate method.

So stay tuned because in the next post of this series we will greatly improve this version of the Tactics game!

As always, the code is available on Bitbucket.


Mijn Twitter profiel Mijn Facebook profiel
Pim Hooghiemstra Webdeveloper and founder of PLint-sites. Loves to build complex webapplications using Vue and Laravel! Latest post
Laravel-mix and the spread operator

Leave a Reply

Your email address will not be published. Required fields are marked *