React Beyond Basics
Target Sum

The Target Sum Game

I named the game we are going to build here The Target Sum. It is a simple one where the UI starts with a random number in the header, the target (42 in the screenshot below), and a list of random challenge numbers below that target (the 6 numbers in the screenshot below).

picture21

4 of the 6 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.

Remember to familiarize yourself with the mechanics of playing the game before you proceed:

The focus of this game is on making things customizable, minimizing the data needed to flow to children components, and correctly working with timers.

Step 1 – Initial Markup and Styles

The initial mock UI markup of this game is simple. We just need to come up with HTML and CSS to look like the screenshot above. I’ve done this part for you.

The game’s starting HTML/CSS template

Here’s the initial top-level Game component that represents the mock UI markup:

class Game extends React.Component {
  render() {
    return (
      <div className="game">
        <div className="help">
          Pick 4 numbers that sum to the target in 15 seconds
        </div>
        <div className="target">42</div>
        <div className="challenge-numbers">
          <div className="number">8</div>
          <div className="number">5</div>
          <div className="number">12</div>
          <div className="number">13</div>
          <div className="number">5</div>
          <div className="number">16</div>
        </div>
        <div className="footer">
          <div className="timer-value">15</div>
          <button>Start</button>
        </div>
      </div>
    );
  }
}

Here’s the CSS I used for this markup:

.game { max-width: 400px; margin: 0 auto; text-align: center; }
.help { color: #666; margin-bottom: 10px; text-align: center; }
.target {
  border: thin solid #999; width: 300px; font-size: 45px;
  text-align: center; display: inline-block;
  background-color: lightblue;
}
.challenge-numbers { width: 100%; margin: 10px auto; }
.number {
  border: thin solid lightgray; background-color: #eee;
  width: 40%; text-align: center; font-size: 36px;
  border-radius: 5px;  margin: 1rem 5%; display: inline-block;
}
.footer {
  display: flex; width: 100%; justify-content: space-between;
}
.timer-value { color: darkgreen; font-size: 150%; }
button:focus { outline: none; }

Step 2 – Extracting Components

As we learned in the previous game, to find candidate components to extract from the initial markup, we need to think about where in the UI multiple elements are going to share the exact same behavior. For this game, 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 is a good sign that we should probably create a component to represent a single number. I will name that component Number.

Since this is a component that will have a props-based event handler, let’s use a class component. This new Number component will need to receive the integer value that it represents:

class Number extends React.Component {
  render() {
    return <div className="number">{this.props.value}</div>;
  }
}

We then use it in the main Game component:

// In the Game's render method:

<div className="challenge-numbers">
  <Number value={8} /> -0
  <Number value={5} />
  <Number value={12} />
  <Number value={13} />
  <Number value={5} />
  <Number value={16} />
</div>

Other components to consider for extraction here might be a Target or a Timer component. However, remember that every time you introduce a new component in the tree you increase the probability of complicating the necessary data flow. Keep things simple until you have good signs that a component will make the code more readable, simpler, or more efficient. I am going to keep this example simple and use only the two components Game and Number.

Playground link for the code so far

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

// Utility function

const randomNumberBetween = (min, max) =>
  Math.floor(Math.random() * (max - min + 1)) + min;

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?

The set of picked random challenge numbers needs to have a subset that actually sums to the random target number we generated. We cannot just pick any random number; we have to pick some numbers based on the factors of the target number and then add 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. In fact, let’s make the Game component receive four new properties to make it entirely customizable:

<Game
  challengeRange={[2, 9]}
  challengeSize={6}
  answerSize={4}
  initialSeconds={15}
/>

The challengeRange is the range we need to use when picking random numbers, the challengeSize is how many random numbers to pick, the answerSize is how many of the random numbers should form a correct pick, and the initialSeconds is how long the game timer should tick.

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 randomly-picked 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! The size of this random subset should be answerSize.

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:

// In the Game class

challengeNumbers = Array.from({
  length: this.props.challengeSize,
}).map(() =>
  randomNumberBetween(...this.props.challengeRange)
);

target = _.sum(
  _.sampleSize(this.challengeNumbers, this.props.answerSize)
);

// In the Game's render method

<div className="help">
  Pick {this.props.answerSize} numbers that sum to the
  target in {this.props.initialSeconds} seconds
</div>

<div className="target">*{this.target}*</div>

<div className="challenge-numbers">
  {this.challengeNumbers.map((value, index) =>
    <Number key={index} value={value} />
  )}
</div>

<div className="footer">
  <div className="timer-value">*{this.props.initialSeconds}*</div>
  <button>Start</button>
</div>

Note how I changed the help message to reflect the new dynamic values. Don’t forget help messages and other strings when you make your components customizable.

Also, note how I used the index from the map function arguments as the key for every Number component returned from the map callback function. 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.

We cannot use the number value itself as they key because the list of random challenge numbers might have repeated values and the key attribute needs to have a unique value per child in the dynamic array. The array index in this case can serve as the unique identifier for each number.

Playground link for the code so far

Step 4 – Deciding What Goes on the State

When the Start button is clicked, the game will move into a different state and the 15-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 probably 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. Let’s name it selecetedIds because we have to identify the numbers by their unique identifiers rather than their value, which can have duplicates.

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:

// In the Game component

state = {
  gameStatus: 'new', // new, playing, won, lost
  remainingSeconds: this.props.initialSeconds,
  selectedIds: [],
};

Note how I initialized a new state element remainingSeconds from a Game’s initialSeconds prop value. Be careful about copying values from props to state and only do it if the prop value is meant to be just the initial value of something that needs to change, like the remainingSeconds example.

To be precise, just like the previous game, 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.

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?

Well, since we decided to have a gameStatus element on the state, we can just use that to lookup the right target’s background color from a dictionary object. We can actually re-use the colors object that we used for the past two games here if we match its keys to the different game statuses that we have. However, we have different words for the keys here so I’ll just use a new colors object:

// Utility colors object

const colors = {
  new: 'lightblue',
  playing: 'deepskyblue',
  won: 'lightgreen',
  lost: 'lightcoral',
};

Playground link for the code so far

Step 5 – Designing Views as Functions of Data and State

Let’s first design some mock states to make testing this step easier. Something like:

// Mock states:

state = {
  gameStatus: 'playing',
  remainingSeconds: 7,
  selectedIds: [0, 3, 4],
};

// Also test with:
  gameStatus: 'lost'

// And:
  gameStatus: 'won'
Remember that 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.

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?

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 this game’s 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.

This is more important than you might think. However, remember that 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.

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.

// In the Number class

render() {
  return (
    <div
      className="number"
      style={{ opacity: this.props.clickable ? 1 : 0.3 }}
      onClick={() => console.log(this.props.id)}
      >
      {this.props.value}
    </div>
  );
}

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:

// In the Game class

isNumberAvailable = numberIndex =>
  this.state.selectedIds.indexOf(numberIndex) === -1;

Let’s put the game status and the remaining seconds in constants rather than reading them from the state each time:

// In the Game's render method

const { gameStatus, remainingSeconds } = this.state;

One behavior you probably noticed while previewing the game 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:

// In the Game's render method

<Number
  key={index}
  id={index}
  value={gameStatus === 'new' ? '?' : value}
  clickable={this.isNumberAvailable(index)}
/>

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

// In the Game's render method

<div
  className="target"
  style={{ backgroundColor: colors[gameStatus] }}
>
  { gameStatus === 'new' ? 'TARGET' : this.target}
</div>

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

// In the Game's render method

<div className="footer">
  {gameStatus === 'new' &&
    <button>Start Game</button>}

  {gameStatus === 'playing' &&
    <div className="timer-value">{remainingSeconds}</div>
  }

  {['won', 'lost'].includes(gameStatus) &&
    <button>Play Again</button>
  }
</div>

Playground link for the code so far

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:

  • Change the gameStatus to playing

  • 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:

// In the Game class

startGame = () => {
  this.setState({ gameStatus: 'playing' }, () => {
    this.intervalId = setInterval(() => {
      this.setState((prevState) => {
        const newRemainingSeconds =
          prevState.remainingSeconds - 1;
        if (newRemainingSeconds === 0) {
          clearInterval(this.intervalId);
          return { gameStatus: 'lost', remainingSeconds: 0 };
        }
        return { remainingSeconds: newRemainingSeconds };
      });
    }, 1000);
  });
};

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 game status. Let’s create a calcGameStatus function to do that.

Here is one way to implement these two new functions:

// In the Game class

selectNumber = numberIndex => {
  this.setState(
    prevState => {
      if (prevState.gameStatus !== 'playing') {
        return null;
      }
      const newSelectedIds =
        [...prevState.selectedIds, numberIndex]
      return {
        selectedIds: newSelectedIds,
        gameStatus: this.calcGameStatus(newSelectedIds),
      }
     },
    () => {
      if (this.state.gameStatus !== 'playing') {
        clearInterval(this.intervalId);
      }
    }
  );
};

calcGameStatus = newSelectedIds => {
  const sumSelected = newSelectedIds.reduce(
    (acc, curr) => acc + this.challengeNumbers[curr],
    0
  );
  if (newSelectedIds.length !== this.props.answerSize) {
    return 'playing';
  }
  return sumSelected === this.target ? 'won' : 'lost';
};

Just like the previous game, since the new gameStatus is to be computed while we are updating the state, I passed the newselectedIds value to the calcGameStatus function rather than using the current selectedIds state value (which would not have been updated to include the new numberIndex at that point).

In calcGameStatus, I used a r`educe` 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 new 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 asynchronous setState call is done.

The game is currently completely functional with the exception of the Play Again button.

Playground link for the code so far

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 t`arget` 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:

// In the Game class

componentWillUnmount() {
  clearInterval(this.intervalId);
}

Now, if the Game component is unmounted and re-mounted, it will start a completely fresh instance with new random challenge 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 (let’s name it GameContainer) and put something on the state of this new parent component (to trigger a full UI update).

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 GameContainer 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 GameContainer 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’s a possible implementation for the new GameContainer component:

// Create new component that renders a game

class GameContainer extends React.Component {
  state = {
    gameId: 1,
  };

  resetGame = () =>
    this.setState(prevState => ({
      gameId: prevState.gameId + 1,
    }));

  render() {
    return (
      <Game
        key={this.state.gameId}
        autoPlay={this.state.gameId > 1}
        challengeRange={[2, 9]}
        challengeSize={6}
        answerSize={4}
        initialSeconds={15}
        onPlayAgain={this.resetGame}
      />
    );
  }
}

Don’t forget to render the new GameContainer component with ReactDOM:

ReactDOM.render(<GameContainer />, mountNode);

Then, make the following moderations to the Game component:

// In the Game class:
// Use the value of the new autoPlay prop to start game

componentDidMount() {
  if (this.props.autoPlay) {
    this.startGame();
  }
}

// In the Game's render method:
// Wire the Play Again action using the parent prop

<button onClick={this.props.onPlayAgain}>
  Play Again
</button>

Playground link for the code so far

Step 8 – Optimizing Measurable Issues

Just like in the previous game, to see if we have any wasteful renders of components for the code so far, we can add a componentWillUpdate method in the Number component and just console.log something there:

// In the Number class

componentWillUpdate() {
  console.log('Number Updated');
}

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!

picture22

The Number component was re-rendered 96 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 90 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.

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. We can use React’s P`ureComponent` for the Number component to take advantage of that.

Go ahead and change the Number component to extend React.PureComponent instead of React.Component and see how the problem goes away.

class Number extends React.PureComponent
picture23

In the screenshot above, exactly 10 Number components were updated (initial 6 + the selected 4).

Do you think the optimizations that we did with PureComponent were worth it? We cannot answer that question without measuring. Basically, we 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.

Playground link for the final code of this game

Bonus Challenges

Here are a few challenges that you can try on your own for this game:

  • Since we pick challenge numbers at random for this game, sometimes the game will render many repeated numbers (for example, four 6s and two 7s for a target of 36). This makes the game sometimes easy to solve. Change the logic so that the game does not render many repeated challenge numbers.

  • For an answer size of 4, sometimes the player can pick 3 numbers that would already be equal or more than the target. Modify the logic so that the game is won or lost for these cases. Right now, the player has to pick exactly 4 numbers.

  • Change the order of challenge numbers every time a number is selected.

A version of this game was featured in my LinkedIn Learning course, React Native Essential Training, where I follow a very similar flow but with React Native instead of React.

What’s Next?

I hope you had fun building the games in this book! From this point on you have many options to explore:

  • This book did not cover some advanced concepts in React. For example, the Context API and SSR. These require working with longer examples and configuring local projects. I go through most of the advanced concepts in React in my Pluralsight course: Advanced React.js

  • You need to learn the Node.js runtime. Node has a lot of packages that will make your job as a React developer much easier. However, in order to master using these packages you need a good knowledge of the Node runtime itself. Check out my book about it: Node.js Beyond the Basics

  • I create interactive and adaptive labs about JavaScript and React at jsComplete.com, these labs might be a fun next step for you to validate what you learned so far and explore new topics

  • You should learn Redux and React-Redux. Redux is a state manager that offers a lot more features than React’s built-in manager of state. It’s a very small library and it’s really easy to learn

  • You should explore GraphQL as an API layer for your data. GraphQL is simply amazing. My next book will actually be about GraphQL but I also have a Pluralsight course about it: Building Scalable APIs with GraphQL

  • You should use your React skills to build a React Native app! You can re-implement the games we built here with React Native. I have actually done exactly that in my LinkedIn Learning course: React Native Essential Training

  • Learn about TypeScript (or FlowType). Adding static-type checking to your JavaScript code will minimize problems and make the code easier to maintain and extend.

  • Learn about Prettier and ESLint and make sure you never ever write a line of JavaScript without having them integrated in your editor. These tools will save you a lot of time. ESLint is a code quality tool and Prettier is code formatting tool. You need both and they work great together. I hope you will be able to build great things with React. Ping me on Twitter and brag about your work!