Book Preview: Learning React by Building Games

A chapter from a React.js book I am currently writing and planning to publish by end-of-year.

Samer Buna
EdgeCoders
Published in
16 min readNov 16, 2017

--

Please note that this article is not a beginner tutorial. I will be assuming that you know the basics of React. If you are absolutely new to React, start by writing your first React component and learn the fundamental concepts of React.

The Target Sum Game: Pick the set of challenge numbers that sum to the target 42 within 10 seconds

I named the game we are going to build in this article The Target Sum. It is a simple one: you start with a random number in the header, the target (42 in the screenshot above), and a list of random challenge numbers below that target (the 6 numbers in the screenshot above).

Four of the six random numbers used above (8, 5, 13, 16) sum up to exactly the target number 42. Picking the correct subset of numbers is how you win the game.

Wanna play a few rounds? Click the Start button below

You will have 10 seconds to click the 4 correct numbers once you click Start

Where you able to win? I am SO bad at this game.

Now that you know what we are going to build, let’s dive right in. Don’t worry, we will build this game in small increments and one step at a time.

Step #1: Initial markup and styles

It is a good idea to start with any known markups and styles and get those out of the way. With simple games like this one, this is usually an easy task. Just put mock static content where the dynamic content will eventually be.

To keep this article as short as possible and focus on React, I will start with some initial ready markup and CSS. Here is a jsComplete code session that you can use to start out with:

Edit code inline here or at https://jscomplete.com/repl/?j=HkGgHj7kM

If you want to follow along with a different development environment, here is all the CSS that I used to style the markup above:

I am not very good with CSS and some of my choices above are probably questionable. Do not get distracted by that; we have a game to build.

Step #2: Extracting components

Once we reach a good state for the initial markup and styles, thinking about components is a natural next step. There are many reasons to extract part of the code into a component. For this example, I would like to focus on just one reason: Shared Behavior.

A good indicator of the need for a new component is when multiple elements are going to share the exact same behavior. In our example, all of the six random challenge numbers will be clickable to sum towards a target number and their clicks will trigger UI changes. This shared behavior means that we should create a component to represent a single number. I will simply name that Number.

The new changes introduced in every code snippet below are highlighted in bold.

You might want to extract more components such as a Target or a Timer component. While adding components like these might enhance the readability of the code, I am going to keep the example simple and use only two components: Game and Number.

Step #3: Making things dynamic

Every time we render a new game, we need to create a new random target number. This is easy; we can use Math.random() to get a random number within the min...max range using this function:

If we need a target number between 30 and 50, we can simply use randomNumberBetween(30, 50).

Then, we need to generate the six random challenge numbers. I am going to exclude the number 1 from these numbers and probably not go above 9 for the first level, so we can simply use randomNumberBetween(2, 9) in a loop to generate all challenge numbers. Easy, right? RIGHT?

This set of random challenge numbers need to have a subset that actually sums to the random target number that we generated. We cannot just pick any random number; we have to pick some factors of the target number (with some of their factorization results) and then some more distracting random numbers. This is hard!

If you were doing this challenge in a coding interview, what you do next might make or break the job offer. What you need to do is to simply ask yourself: is there an easier way?

Take a minute and think about just this problem. To make things interesting, let’s make the size of the challenge numbers list dynamic. The Game component will receive two new properties:

The simple alternative to the factorization problem above is to pick the random challenge numbers first and then compute the target from a random subset of these challenge numbers.

This is easier. We can use Array.from to create an array of random numbers with the help of the randomNumberBetween function. We can then use the lodash sampleSize method to pick a random subset and then just sum that subset and call it a target!

Since all of these numbers are not going to change during a single game session, we can safely define them as instance properties.

Here are the modifications that we need so far:

Note how I used the index value from the map call as the key for every Number component. Remember that this is okay as long as we are not deleting, editing, or re-arranging the list of numbers, which we will not be doing here.

You can see the full code we have so far here.

Step #4: Deciding what goes on the state

When the Start button is clicked, the game will move into a different state and the 10 second timer will start its countdown. Since these are UI changes, a game status and the current value of that timer at any given time should be placed on the state.

When the game is in the playing mode, the player can start clicking on challenge numbers and every click will trigger a UI change as well. When a number is selected, we need the UI to represent it differently. This means we also need to place the selected numbers on the state as well. We can simply use an array for those.

However, we cannot use the number values in this new array because the list of random challenge numbers might have repeated values. What we need to designate as selected are the unique IDs of these numbers. We used a number positional index as its ID so we can use that to uniquely select a number.

All of these identified state elements can be defined on the state of the Game component. The Number component does not need any state.

Here is what we need to place on the Game component state so far:

Note how I made the initial value for the number of remainingSeconds customizable as well and used a new game-level prop (initialSeconds) for that:

To be honest, we do not need the gameStatus to be on the state at all. It is mostly computable. However, placing it on the state is an exception that I am intentionally making as a simplified form of caching that computation. Ideally, caching this computation is better done as an instance property, but I will keep it on the state to keep things simple.

What about the background colors used for the target number when the player wins or loses a game? Do those need to go on the state?

Not really. Since we have a gameStatus element, we can use that to lookup the right background color. The dictionary of background colors can be a simple static Game property (or you can pass it down if you want to make it customizable):

You can see the full code we have so far here.

Step #5: Designing views as functions of data and state

This is really the core of React. Now that we identified all of the data and state this game needs, we can design the whole UI based on them.

Since the state usually starts with empty values (like the empty selectedIds array), it is hard to design the UI without testing using actual values. However, mock values can be used to make testing easier:

Using this strategy, we do not have to worry about behavior and user interactions (yet). We can focus on just having the UI designed as functions of data and (mock) state.

The key to doing this step correctly is to make sure child components receive only the minimum data that they actually need to re-render themselves in the various states. This is probably the most important statement in the entire article.

We only have one child component, so let’s think about what it needs to render itself. We are already passing down its value from the map call so what else does it need? For example, think about these questions:

  • Does the Number component need to be aware of the selectedIds array to figure out whether it is a selected number?
  • Does the Number component need to be aware of the current gameStatus value?

I will admit that answering these questions is not as easy as one might think. While you might be tempted to answer yes to them, the Number component does not need to be aware of both selectedIds and gameStatus. It only needs to be aware of whether or not it can be clicked because if it cannot be clicked it will need to render itself differently.

Passing anything else to the Number component will make it re-render unnecessarily, which is something we should avoid.

We can use a lower opacity to represent a non-clickable number. Let’s make the Number component receive a clickable prop.

Computing this boolean clickable prop should happen in the Game component to avoid having to pass more data to the Number component. Let me give examples about the importance of making sure a child component receives only the minimum data that it needs:

  • If we pass the gameStatus value to the Number component, then every time the gameStatus changes (for example, from playing to won), given the example numbers we are testing with, React will re-render all six challenge numbers while it did not really need to re-render any of them for that case. A Number component does need to re-render when the gameStatus changes from new to playing because of the masking question marks feature at the beginning. To avoid passing down the gameStatus to Number, we can compute the value displayed in a Number component within the map function callback in the Game component.
  • If we pass the selectedIds array down to the Number component, then on every click React will re-render all six challenge numbers when it only needed to re-render one number. This is why a clickable boolean flag is a much better choice here.

With every prop you pass to a child React component comes great responsibility.

This is more important than you might think. However, React will not optimize the re-rendering of a component automatically. We will have to decide if we want it to do so. This is discussed in step #8 below.

Besides the clickable prop, what else does the Number component need? Since it is going to be clicked and we need to place the clicked number’s ID on the Game state, the click handler of every Number component needs to be aware of its own ID (and we cannot use React’s key prop value there). Let’s make the Number component receive an id prop as well.

The computation of whether a number is available and clickable can use a simple indexOf call on the selecetdIds array. Let’s create a function for that:

One behavior you probably noticed while playing the game above is that the number squares start out displaying a question mark until the Start button is clicked. We can use a ternary operator to control the value of each Number component based on the gameStatus value. Here is what we need to change to render a Number component inside the map call:

We can use a similar ternary expression for the target number value and also control its background color using a lookup call to the static bgColors object:

Finally, we should show the Start button only when the gameStatus is new and otherwise show the remainingSeconds counter. When the game is won or lost, let’s show a Play Again button. Here are the modifications we need for all that:

You can see the full code we have so far here.

Step #6: Designing behaviors to change the state

The first behavior that we need to figure out is how to start the game. We need two main actions here: 1) change the gameStatus to playing and 2) start a timer to decrement the remainingSeconds value.

If remainingSeconds is decremented all the way to zero, we need to force the game into the lost state and stop the timer as well (because otherwise, it will decrement beyond zero.)

Here is a function we can use to do all that:

Note how I start the timer only after the setState call is complete using the second argument function callback to setState.

Next, let’s figure out what should happen when a number is clicked during a game session. Let’s create a selectNumber function for that. This function should receive the ID of the clicked number and only work when the gameStatus is playing. Every time a number is clicked, we need to add its ID to the selectedIds array. We also need to compute the new gameStatus because every click might result in a won/lost status. Let’s create a calcGameStatus function to do that.

Here is one way to implement these two new functions:

Note a few things about the functions above:

  • We used the array spread operator to append numberIndex to selectedIds. This is a handy trick to avoid mutating the original array.
  • Since the new gameStatus is to be computed while we are updating the state, I passed the new selectedIds value to the calcGameStatus function rather than using the current selectedIds value (which has not been updated yet to include the new numberIndex at that point).
  • In calcGameStatus, I used a reduce call to compute the current sum after a click using a combination of what is selected and the original challengeNumbers array, which holds the actual values of numbers. Then, a few conditionals can do the trick of determining the current game status.
  • Since the timer has to be stopped if the new gameStatus is not playing, I used the second callback argument for setState to implement that logic and make sure it will use the new gameStatus after the async setState call is done.

The game is currently completely functional with the exception of the Play Again button. You can see the full code we have so far here.

Now, how exactly are we going to implement this Play Again action? Can we simply just reset the state of the Game component?

Nope. Think about why.

Step #7: Resetting a React component

The Play Again action needs more than a simple reset of the state of the Game component. We need to generate a new set of challengeNumbers and target number. In addition, we need to clear any currently running timers and auto-start the game.

We can certainly improve the startGame function to do all of that, but React offers an easier way to reset a component: unmount that component and just remount it. This will trigger all initialization code and take care of any timers as well.

We do not really have to worry about the timer part of the state because that part is controlled by behavior. However, in general, unmounting a component should also clear any timers defined in that component. Always do that:

Now, if the Game component is unmounted and re-mounted, it will start a completely fresh instance with new random numbers and an empty state. However, to re-mount a component based on a behavior, we will need to introduce a new parent component for Game (I will name that App) and put something on the state of this new parent component (to trigger a UI change).

React has another useful trick we can use to accomplish this task. If any React component is rendered with a certain key and later re-rendered with a different key, React sees a completely new instance and automatically unmounts and re-mounts that component!

All we need to do is have a unique game ID as part of the state of the App component, use that as the key for the Game component, and change it when we need to reset a game.

Since we also want the game to auto-start when the player clicks Play Again (instead of having them click Start after Play Again), let’s make the App component also pass down an autoPlay prop to Game and compute that based on the new gameId attribute. Only the first game should not be auto played.

Here are the modifications that we need:

You can see the full code we now have here.

Step #8: Optimize if you can measure

Wasteful rendering of the components that do not need to be re-rendered is one of the challenging aspects of a React application. We went to great lengths in step #5 to not pass any prop that will cause a Number component to re-render unnecessarily.

However, the code as it is now is still wastefully re-rendering most of the Number components. To see this in action, use a componentWillUpdate method in the Number component and just console.log something there:

Then, go ahead and play. On every state change in the Game component, you will see that we are re-rendering all 6 Number components. This happens when we click the Start button and every second after that!

The Number component was re-rendered 66 times. How many of these were necessary?

The fact is, a Number component should not re-render itself unless the player clicks on it. The 60 re-renders that were triggered by the timer change were wasteful. Furthermore, when the player clicks a number, only that number needs to be re-rendered. Right now, React also re-renders all six numbers when the player selects any number.

36 Number component updates happened when only 9 updates should have happened

Luckily, we have been careful enough to only pass to the Number component the exact props that it needs to re-render. Only the challenge number that needs to be re-rendered will receive different values in these props. This means we can use a conditional in React’s shouldComponentUpdate to short-circuit the render operation if all nextProps of a Number component match the current props.

React’s PureComponent class will do exactly that. Go ahead and change the Number component to extend React.PureComponent instead of React.Component and see how the problem magically goes away.

Exactly ten Number components were updated (initial six + the selected four)

However, is this optimization worth it? We cannot answer that question without measuring. Basically, you need to measure which code uses fewer resources: a component render call or the if statement in React.PureComponent that compares previous and next state/props. This completely depends on the sizes of the state/props trees and the complexity of what is being re-rendered. Do not just assume one way is better than the other.

You can see the final code here. MVP complete. Now, for the love of CSS, can someone please style this game to make it appealing to kids? :)

Do not stop here if you like this. Add more features to the game. For example, keep a score for winning and increase it every time the player wins a round. Maybe make the score value depend on how fast the player wins the round. You can also make future rounds harder by changing challengeSize, challengeRange, and initialSeconds when starting a new game.

The Target Sum game was featured in my React Native Essential Training course, which is available on Lynda and LinkedIn Learning.

Thanks for reading.

I dedicated a big part of my 2017 to writing books which are all now available on Amazon:

--

--

Samer Buna
EdgeCoders

Author for Pluralsight, O'Reilly, Manning, and LinkedIn Learning. Curator of jsComplete.com