CHAPTER 5
In the previous chapter, we learned a few basic concepts about React components, including how to define them, make them receive props, render them in the browser, make them interactive, and make them reusable. However, we have been using abstract examples so far. Let’s build something a little bit more interesting (and fun).
I love memory games, so let’s build a simple one in this chapter. I picked a grid-based memory challenge game where the player is challenged to recall the locations of cells on a grid. Here’s what the UI will look like when we’re done:

Figure 7: The memory challenge game
Here’s how this game works:
Note: Familiarize yourself with the mechanics of playing the game before you proceed. You can play the final version here.
Let’s go through this game one step at a time. The key strategy is to find small increments and focus on them, rather than getting overwhelmed with the whole picture. Don’t shoot for perfection from the first round. Have something that works and then iterate, improve, and optimize.
“Make it work. Make it right. Make it fast.”
—Kent Beck
I like to start a React app like this one with some initial markup and any styles that I can come up with for that markup. I do that using a single React component. That’s usually a good starting point that’ll make it easier to think about the structure of the app components and which elements will need to have a dynamic aspect to them.
The initial template for this game will render a simple empty grid of white cells, a message, and a button. Here’s the HTML and CSS that we’ll start with. This will render a 3 × 3 grid:
Code Listing 87: The HTML | jscomplete.com/playground/rs3.1
const Game = () => { return ( <div className="game"> <div className="grid"> <div className="cell" style={{ width: '33.3%' }} /> <div className="cell" style={{ width: '33.3%' }} /> <div className="cell" style={{ width: '33.3%' }} /> <div className="cell" style={{ width: '33.3%' }} /> <div className="cell" style={{ width: '33.3%' }} /> <div className="cell" style={{ width: '33.3%' }} /> <div className="cell" style={{ width: '33.3%' }} /> <div className="cell" style={{ width: '33.3%' }} /> <div className="cell" style={{ width: '33.3%' }} /> </div> <div className="message">Game Message Here...</div> <div className="button"> <button>Start Game</button> </div> </div> ); }; ReactDOM.render(<Game />, mountNode); |
Code Listing 88: The CSS | jscomplete.com/playground/rs3.1
*, *:before, *:after { box-sizing: border-box; } .game { max-width: 600px; margin: 5% auto; } .grid { border: thin solid gray; line-height: 0; } .cell { border: thin solid gray; display: inline-block; height: 80px; max-height: 15vh; } .message { float: left; color: #666; margin-top: 1rem; } .button { float: right; margin-top: 1rem; } |
A few things to note about this code:
We’ll also need a few utility math functions for this game. To keep the focus on React code in this example, I’ll provide these utility functions for you here. Their implementation is not really important in the context of this React example, but it’s always fun to read their code and try to understand what they do (but let’s do that when we start using them).
Code Listing 89: The math science utility functions | jscomplete.com/playground/rs3.1
// Math science const utils = { /* Create an array based on a numeric size property. Example: createArray(5) => [0, 1, 2, 3, 4] */ createArray: size => Array.from({ length: size }, (_, i) => i), /* Pick random elements from origArray up to sampleSize And use them to form a new array. Example: sampleArray([9, 12, 4, 7, 5], 3) => [12, 7, 5] */ sampleArray: (origArray, sampleSize) => { const copy = origArray.slice(0); const sample = []; for (let i = 0; i < sampleSize && i < copy.length; i++) { const index = Math.floor(Math.random() * copy.length); sample.push(copy.splice(index, 1)[0]); } return sample; }, /* Given a srcArray and a crossArray, Count how many elements in srcArray exist or do not exist in crossArray. Returns an array with the two counts. Example: arrayCrossCounts([0, 1, 2, 3, 4], [1, 3, 5]) => [2, 3] */ arrayCrossCounts: (srcArray, crossArray) => { let includeCount = 0; let excludeCount = 0; srcLoop: for (let s = 0; s < srcArray.length; s++) { for (let c = 0; c < crossArray.length; c++) { if (crossArray[c] === srcArray[s]) { includeCount += 1; continue srcLoop; } } excludeCount += 1; } return [includeCount, excludeCount]; }, }; |
I’ll explain these utility functions as we use them.
Note: The code for all the HTML, CSS, and utility functions is available here.
Once we reach a good state for the initial markup and styles, thinking about components is the natural next step.
There are many reasons to extract a section of the code into a component. Here are a couple in the context of our game:
We extract components for other reasons too. For example, when there is duplication of sub-trees, or when we need to make parts of the full tree reusable in general. This is a simple game with no shared sub-trees, so we don’t need to extract any components for these reasons.
There is another very important reason to extract a component, and that’s to split complex logic into smaller, related units. This is usually done once the code crosses the boundaries of the level of complexity that’s acceptable to you. At that point, it’ll be easier for you to decide what related units should be extracted on their own. We’ll see an example of that soon.
Extracting a component is simply taking part of the tree as is, making it the return value of the new component (for example, Cell), and then using the new component in the exact place where the extracted part used to be. To use the new Cell component, you would include a call to <Cell />.
Here’s the code we started with after I extracted Cell and Footer out of Game:
Code Listing 90: Code available at jscomplete.com/playground/rs3.2
const Cell = () => { return ( <div className="cell" style={{ width: '33.3%' }} /> ); } const Footer = () => { return ( <> <div className="message">Game Message Here...</div> <div className="button"> <button>Start Game</button> </div> </> ); }; const Game = () => { return ( <div className="game"> <div className="grid"> <Cell /> <Cell /> <Cell /> <Cell /> <Cell /> <Cell /> <Cell /> <Cell /> <Cell /> </div> <Footer /> </div> ); }; |
The represented DOM tree did not change—only its arrangement in React’s memory did. Note how I needed to use a React.Fragment (<></>) around the two divs in Footer because they’re now abstracted into a new sub-tree represented with a single React element. We didn’t need that before because they were part of the main tree.
In the new Cell component, we can now define the generic shared logic for both displaying a grid cell and for the behavior of each grid cell in this game.
You might want to extract more components, maybe a Row or GameMessage component. While adding components like these might enhance the readability of the code, I am going to keep the example simple and start with just these three components.
Tip: You might feel that this game is a bit challenging to think about all at once. It is! It has many states and many interactions, but don’t let that distract you. What you need is to focus your thinking into finding your next simple "increment." Find the tiniest possible change that you can validate, just like "extracting part of the initial markup into a component." That was a good, small increment that we just validated. We didn’t worry about anything else in the game while focusing on that extracting increment. Now you need to think about the next increment and focus on just that. The smaller the increments you choose, the better.
An increment could be anything. There is no right answer. You could focus on defining a click handler on Cell and make sure it fires correctly, or you could think about the first three-second timer that’ll start ticking when the Start Game button is clicked. However, some increments will depend on others. Try to identify the increments that you’re ready for. For example, a great next increment for us at this point is to get rid of the manually repeated Cell lines and drive that through a dynamic value.
Note: The playground session for the code so far can be found here.
Imagine needing to pass a prop value to each Cell element. Right now, we would need to do that in nine places. It’ll be a lot better if we generated this list of grid cells with a dynamic loop. This will enhance the readability and maintainability of the code.
In general, while you’re making progress in your code, you should keep an eye on values that can be made dynamic. For example, unless the requirement is to have a fixed grid that is always 3 × 3, we should introduce a gridSize property somewhere and use it to generate a gridSize * gridSize grid of Cell components. Don’t hard-code the number nine (or 16 or 25) into the loop code. Hard-coded values in your code are usually a "smell" (bad code). Even if you don’t need things to be dynamic, just slapping a label on a hard-coded number makes the code a bit more readable. This is especially important if you need to use the same number (or string, really) in many places.
Let’s test things out with a gridSize of 5. Instead of defining this as a variable in the Game component, I’ll make it into a "prop" that the top-level component receives. This just an option that’s available to you when you need it, but in our case it delivers an extra value! Besides not having the number 5 hard-coded (within the loop code), we can also render the Game component with a different grid size just by changing its props.
Code Listing 91
// In the ReactDOM.render call <Game gridSize={5} /> |
Tip: Note again how I used {5} and not "5" because the Game component should receive this prop as a number value, not as a string value.
Can you think of more primitive values that we should not hard-code in the code? How about the number of cells to highlight as challenge cells? The maximum wrong attempts allowed? The number of seconds to highlight the challenge cells? The number of seconds that’ll cause the game to time out? All of these should really be made into variables in the code. I’ll make them all as props to the Game component.
Code Listing 92
// In the ReactDOM.render call <Game gridSize={5} challengeSize={6} challengeSeconds={3} playSeconds={10} maxWrongAttempts={3} /> |
We can use a simple for loop to generate a list of Cell components based on the new gridSize prop and give every cell a unique ID based on the loop index. However, the declarative way to do this is to create an array of unique IDs first, and then map that into an array of Cell components.
The value of doing so is really beyond just being declarative. Having an array of all cell IDs will simplify any calculations that we need. For example, picking a list of random challenge cells becomes a matter of sampling this generated array.
We need an array with a length of gridSize * gridSize. There are many ways to do this in JavaScript, but my favorite is to use the Array.from method. This method can be used to initialize a generated array using a callback function. In the provided utils object, the createArray function uses this method to generate an array of any size and have its elements initialized as the index value for their positions.
We can use this utils.createArray function to create the grid cell IDs array. I will name that array cellIds. This is a fixed array for each game, which means we don’t need to make it part of the game state:
Code Listing 93
const Game = ({ gridSize }) => { const cellIds = utils.createArray(gridSize * gridSize); // ... }; |
Note: I’ll be using the comment // … to mean "omitted code."
Note how I destructured the gridSize prop and used it. We’ll need to destructure the other props as well, but we’ll do that when we need them.
With this array, we can now use the Array.map method to convert the array of numbers into an array of Cell elements.
Code Listing 94
const Game = ({ gridSize }) => { // ... <div className="grid"> {cellIds.map(cellId => <Cell key={cellId} /> )} </div> // ... }; |
Note: I used the values in the cellIds array as keys for Cell. This not the same as using the loop index as the key. These values are unique IDs in this context. We’re also not re-ordering or deleting any of these cells, so these sequential numbers are sufficient for their elements’ identities.
With all the changes up to this point, the app will render a grid of 5 × 5, but we are still using a fixed width of 33.3% for each cell. We need to compute the width we need for each grid size: 100/gridSize. We can pass that value as a prop to the Cell component.
We could do something like:
Code Listing 95
<Cell key={cellId} width={100/gridSize} /> |
However, this new width property has a small problem. Think about it. How can we do this in a better way?
The 100/gridSize computation, while fast, is still a fixed value for each rendered game. Instead of doing it 25 times (for a gridSize of 5), we can do it only once before rendering all the Cell elements and just pass the fixed value to all of them:
Code Listing 96
const Game = ({ gridSize }) => { // .. const cellWidth = 100 / gridSize; // ... <div className="grid"> {cellIds.map(cellId => <Cell key={cellId} width={cellWidth} /> )} // ... }; |
Tip: I don’t classify the cellWidth change here as a premature optimization. I classify it as an obvious one. It will probably not improve the performance of the code by much, but it is the type of change that we can make with 100 percent confidence that it’s better. I also think it makes the code more readable. One rule of thumb to follow here is to try and extract any computation done for a value passed to a prop into its own variable or function call.
We can now use the new width prop in the Cell component instead of the previously hard-coded 33.3%:
Code Listing 97
const Cell = ({ width }) => { return ( <div className="cell" style={{ width: `${width}%` }} /> ); }; |
All the changes we made so far are part of the core concept in React that the UI is a function of data and state. However, our UI functions so far have been using only fixed-value data elements (like gridSize). We have not placed anything on the state yet; we will do that shortly. We will also add more fixed-value data elements. Sometimes, it’s challenging to figure out if a data element you’re considering should be a state one or a fixed-value one. Let’s talk about that next.
Note: The playground session for the code so far can be found here.
What’s a good next increment now? Let’s think about what we need to place on the state of this game.
When the user clicks the Start Game button, the game UI will move into a different status. A number of random cells will need to be marked as challenge cells. The UI will need to be updated to display these challenge cells differently. The game "status" is something that all three components need. The Cell component needs it as part of the logic to determine what color to use. The Footer component needs it to determine what message and button to show. The Game component needs it to determine what timers to start and stop, among a few other things.
This means we need to use a state element to represent this game status in the Game component itself, because it’s the common parent to the other two components. I’ll name this state element gameStatus, and it will have five different values. Let’s give each one a label:
We can place this gameStatus variable on the Game component’s state with a useState call. The initial value is NEW:
Code Listing 98
const Game = ({ gridSize }) => { const [gameStatus, setGameStatus] = useState('NEW'); // ... } |
When the user clicks the Start Game button, we can use setGameStatus to change the gameStatus variable to CHALLENGE. However, since we moved the button into a new Footer component, and its behavior needs to change a state element on the Game component, we need to create a function in the Game component for this purpose and flow it down to the Footer component. I’ll just use an inline function:
Code Listing 99
const Game = ({ gridSize }) => { // ... <Footer startGame={() => setGameStatus('CHALLENGE')} /> // ... }; const Footer = ({ startGame }) => { // ... <div className="button" onClick={startGame} > <button>Start Game</button> </div> // ... }; |
At this point, we’ll need to highlight the challenge cells. However, before we do that, let’s think a little bit more about the gameStatus values we used.
Using strings as labels creates a small problem. If you make a typo in these strings somewhere in the code, that typo will go undetected until you realize that things are not working (in the UI probably) and try to find that typo manually. Many developers can tell countless stories of the time they wasted on a silly typo.
A better way of doing this is to introduce an ENUM holding the specific values for gameStatus. This way, if you use an invalid ENUM value, the problem will be a lot easier to find (especially if your editor uses a type checker like TypeScript).
Unfortunately, JavaScript does not have an ENUM data structure, but we can use a simple object to represent one:
Code Listing 100
const GameStatus = { NEW: 'NEW', CHALLENGE: 'CHALLENGE', PLAYING: 'PLAYING', WON: 'WON', LOST: 'LOST', }; // ... const Game = ({ gridSize }) => { const [gameStatus, setGameStatus] = useState(GameStatus.NEW); // ... <Footer startGame={() => setGameStatus(GameStatus.CHALLENGE)} /> // ... }; |
The values can be anything. You can use numbers or symbols, but simple strings there are okay too.
Similarly, we should introduce an ENUM for the different cell statuses. Let’s give each a label as well:
Since these cell statuses directly drive the color values, I’ll make their ENUM value the colors themselves:
Code Listing 101
const CellStatus = { NORMAL: 'white', HIGHLIGHT: 'lightblue', CORRECT: 'lightgreen', WRONG: 'pink', }; |
This way, in the Cell component, given that we compute the cell status correctly, we can just use its value as the backgroundColor value:
Code Listing 102
const Cell = ({ width }) => { let cellStatus = CellStatus.NORMAL; // Compute the cell status here... // ... <div className="cell" style={{ width: `${width}%`, backgroundColor: cellStatus }} /> // ... }; |
The last ENUM we need for this game is for the game messages. Should these messages be placed on the state? Not really. These messages will be directly derived from the gameStatus value as well. We can actually include them in the GameStatus ENUM itself, but I’ll just give them their own ENUM for simplicity.
For each gameStatus value, let’s give the player a helpful hint or let them know if they won or lost:
Code Listing 103
const Messages = { NEW: 'You will have a few seconds to memorize the blue random cells', CHALLENGE: 'Remember these blue cells now', PLAYING: 'Which cells were blue?', WON: 'Victory!', LOST: 'Game Over', }; |
Now the content of the message div can simply be: Messages[gameStatus].
Because we moved the message div into the Footer component, we’ll need to flow the gameStatus variable from Game into Footer to be able to look up the game message in Footer. We could alternatively look up the game message in the Game component and flow that to the Footer component, but I think it’s cleaner to have that logic in the Footer component. Besides, the rendering of the Footer button will also depend on gameStatus.
Code Listing 104
const Footer = ({ gameStatus, startGame }) => { // ... <div className="message">{Messages[gameStatus]}</div> // .. }; const Game = ({ gridSize }) => { // .. <Footer gameStatus={gameStatus} startGame={() => setGameStatus(GameStatus.CHALLENGE)} /> // ... }; |
Note: Note that if the Messages ENUM needs to be fetched from an API (for example, to use a different language), then we need to place it on the state or delay the render process until we have it.
Let’s now think about how to highlight the challenge cells. When the game starts, we need to randomly pick challengeSize elements from the grid array that has all of the cells' IDs (cellIds). I’ll put these in a challengeCellIds array.
To compute this array, we can use the provided utils function sampleArray, which takes an origArray and a sampleSize, and returns a new array with sampleSize elements randomly picked from origArray.
The important question now is: should we place this challengeCellIds array on the state of the Game component? To answer this question, think about whether or not this challengeCellIds array will change during a game session.
It will not—it is another a fixed value that does not need to be part of the game state. Deciding on the optimal elements that need to be placed on a component state is one of the most important skills of a React developer.
The challengeCellIds array can be a variable in the Game component:
Code Listing 105
const Game = ({ gridSize, challengeSize }) => { const [gameStatus, setGameStatus] = useState(GameStatus.NEW); const cellIds = utils.createArray(gridSize * gridSize); const cellWidth = 100 / gridSize; const challengeCellIds = utils.sampleArray(cellIds, challengeSize); // ... }; |
Tip: Always place the state element first, and then introduce any "computed" ones after that.
The Game component is now aware of which cells it should highlight when the gameStatus becomes CHALLENGE.
However, since challengeCellIds (and cellIds) are computed from props and do not depend on the state of the Game component, co-locating them with the stateful elements in the Game component is a problem.
Note: Interview question: Explain the problem with co-locating the challengeCellIds array with the stateful elements in the Game component.
To see the problem I am talking about, log the value of challengeCellIds after the line that defines it:
Code Listing 106
const Game = ({ gridSize, challengeSize }) => { // ... const challengeCellIds = utils.sampleArray(cellIds, challengeSize); console.log(challengeCellIds); // ... |
Try to render the Game component a few times. On each render, we get a different set of challengeCellIds. That’s great—the sampleArray function is working as expected.
Now, render a game session and click Start Game. What’s your observation?
The challengeCellIds array was regenerated when we clicked that button.

Figure 8: This should not happen
That’s not good: this challengeCellIds should be a fixed value during a single game session. Once it’s generated, it should not change. It should only be re-generated when we need a new game session (with the Play Again button).
Why is this happening? Didn’t we define challengeCellIds using the const keyword? How is it changing when we click the button?
Keep in mind that each time you invoke a useState updater function, which is what we’re doing in the onClick handler of the button using the setGameStatus updater function, React will re-render the whole component that asked for that state. In this case, it is the Game component.
Re-rendering means React will call the Game function again, discard all variables that were defined in the previous render, and create new variables for the new render. It’ll create a new challengeCellIds variable (and new cellIds and cellWidth variables).
These three variables should not be re-created. They should be "immutable" once a game "instance" is rendered. One way of doing that is to make them into global variables. However, that means when we’re ready to implement the Play Again button, all new games will use the exact same challenge cells. That’s not good either.
We want a game session’s fixed values (cellIds, challengeCellIds, and cellWidth) to be global to that session, but not to the whole app. This means we can’t have the Game component as a top-level one. We’ll need a "game generator" component that renders (and later re-renders) a game session. That new component can be responsible for the app-global variables that are needed for each top-level render.
Tip: We can also implement the challengeCellIds using a React.useRef Hook. This Hook gives us an element that’ll remember its value between component renders. However, wrapping the stateful component with another one is a much simpler way.
The problem starts with the fact that I am mixing many responsibilities in the one component that I named Game. Naming matters here—a lot! What exactly did I mean by Game? Is it referring to the game in general, the app that renders many game sessions, or the single playable game session?
The solution starts with better names. This app should have a GameSession component and a GameGenerator component that’s responsible for generating and rendering a game session. When the GameGenerator component is rendered, it can compute the global values needed to run a GameSession. The GameSession component needs to manage its state independently of the state of the GameGenerator component.
Go ahead and try that on your own first. Rename Game to GameSession and keep the console.log line in there for testing. Create a GameGenerator component and move the computing of all three fixed values into it. Then pass them as props to the GameSession component. Test that the challengeCellIds array does not change when you click the Start Game button.
Here are the changes we need to make:
Code Listing 107
const GameSession = ({ cellIds, challengeCellIds, cellWidth, challengeSize, challengeSeconds, playSeconds, maxWrongAttempts, }) => { // ... // Remove lines for cellIds, cellWidth, and challengeCellIds console.log(challengeCellIds); // ... }; const GameGenerator = () => { const gridSize = 5; const challengeSize = 6; const cellIds = utils.createArray(gridSize * gridSize); const cellWidth = 100 / gridSize; const challengeCellIds = utils.sampleArray(cellIds, challengeSize); return ( <GameSession cellIds={cellIds} challengeCellIds={challengeCellIds} cellWidth={cellWidth} challengeSize={challengeSize} challengeSeconds={3} playSeconds={10} maxWrongAttempts={3} /> ); }; ReactDOM.render(<GameGenerator />, mountNode); |
Note a few things about these changes:
Now when you click the Start Game button, the challengeCellIds array will be exactly the same because it’s just a prop that was not changed. React re-renders the GameSession component with its exact same props when its state changes.

Figure 9: Re-rendering Game is now okay
Note: The playground session for the code so far can be found here.
To be able to compute a cell status, we need to design the structure to identify a cell as a correct or wrong pick. Should we have correctCellIds and wrongCellIds arrays managed on the state of the GameSession component?
We could, but a good practice here is to minimize the elements you make stateful in a component. If something can be computed, don’t place it on the state. Take a moment and think about what structure can be the minimum here to enable us to compute a correct or wrong pick.
These statuses will appear when the player starts clicking on cells to pick them. With each pick, something has to change on the state to make React re-render the chosen cell and enable us to recompute its status, which will be either CellStatus.CORRECT or CellStatus.WRONG.
However, we don’t need to manage a structure for correct and wrong picks because both of them can be computed from challengeCellIds. Just knowing that a cell was picked allows us to determine if that pick was correct or wrong. All we need to place on the state is the fact that the cell was picked!
Let’s use an array to keep track of pickedCellIds on the state of the GameSession component. This array starts as empty:
Code Listing 108
const GameSession = ({ gridSize, challengeSize }) => { const [gameStatus, setGameStatus] = useState(GameStatus.NEW); const [pickedCellIds, setPickedCellIds] = useState([]); // ... }; |
We will later design the click behavior on each cell so that it adds the clicked cell’s ID to this pickedCellIds array.
What about winning or losing the game? Do we need to place anything else on the state to make the UI render differently for these states? The answer is no.
Both of these cases are also computable. With a challengeSize of 6 and a maxWrongAttempts of 3, if we have six correctly guessed cells after every guess, the game is won. If we have three wrong attempts, the game is lost. We actually do not even need to change the gameStatus to WON or LOST because these two statuses are computable. However, computing these statuses requires some array math, and placing them on the state is a form of simply caching this computation.
Tip: Ideally, caching the won/lost gameStatus computation is better done with a React ref object (using the React.useRef Hook). However, that’s a bit of an advanced optimization. I will store these values on the gameStatus state for simplicity and consistency. Having part of gameStatus on the state and the other part on a ref object might be confusing.
What about the game timers? Do we need to place anything on the state for them? The answer depends on what we want to display in the UI. If all we need is a timer ticking in the background that drives nothing in the UI to be changed (on every tick), then we do not need to place anything on the state.
However, if we’d like to show a "countdown" value in the UI as the timer is ticking, then that timer needs to update a state element. Let’s do that for the playSeconds timer. Let’s show the countdown from playSeconds to 0 in the UI. We’ll need a state element to hold these countdown values. I’ll name it countdown. Its initial value is the playSeconds prop:
Code Listing 109
const GameSession = ({ cellIds, challengeCellIds, cellWidth, challengeSize, challengeSeconds, playSeconds, maxWrongAttempts, }) => { const [gameStatus, setGameStatus] = useState(GameStatus.NEW); const [pickedCellIds, setPickedCellIds] = useState([]); const [countdown, setCountdown] = useState(playSeconds); // ... }; |
I think we have all the data and states we need for a game session. This is the easy part though—the next few steps are where the real power of React comes in handy. We can complete the design of our UIs as functions of data and state, and then only worry about changing the state afterwards, eliminating the need to do any manual DOM operations.
Note: The playground session for the code so far can be found here.
Now that we have identified and designed all the static and dynamic data elements this game needs, we can continue the task of implementing the UI as a function of these elements. Everything rendered in the UI is a direct map of a certain snapshot of the data and state elements we designed.
This step requires implementing a few computed properties. For example, we need to compute each cell status based on the gameStatus value, the challengeCellIds value, and the pickedCellIds value. Each cell status depends on all three variables.
To keep the code simple, I’ll compute each cell status in the Cell component itself. Does this mean we need to flow gameStatus, challengeCellIds, and pickedCellIds from the GameSession component to the Cell component?
Code Listing 110
// In the GameSession component {cellIds.map(id => ( <Cell key={id} width={cellWidth} gameStatus={gameStatus} challengeCellIds={challengeCellIds} pickedCellIds={pickedCellIds} /> ))} |
No. Don’t do that.
The Cell component does not need to know about all challengeCellIds or all pickedCellIds. It only needs to know whether it is a challenge cell or a picked cell. What we need are flags like isPicked and isChallenge, and we can use a simple Array.includes to compute them:
Code Listing 111
// In the GameSession component {cellIds.map(id => ( <Cell key={id} width={cellWidth} gameStatus={gameStatus} isChallenge={challengeCellIds.includes(id)} isPicked={pickedCellIds.includes(id)} /> ))} |
I’ll refer to this pattern as props minimization. It is mostly a readability pattern, but it is also tied to the responsibility and maintainability of each component. For example, if we later decided to use a different data structure to hold challengeCellIds, ideally the code inside the Cell component should not be affected.
Another way to think about this pattern is to ask yourself this question about each prop you pass to a component: Does this component need to be re-rendered when the value of this prop changes?
For example, think about the pickedCellIds prop we first considered passing to each Cell. This array will be changed on each click of a cell. Since we’re re-rendering a gird of cells (25 cells, for example), the props-minimization question becomes: do all 25 grid cells need to be re-rendered every time we click on a cell?
The answer is no, only one cell needs to be re-rendered: the one cell that will change its isPicked value from false to true.
With every prop you pass to a child component comes great responsibility!
Tip: React will re-render all components in a sub-tree when the parent component re-renders. So, although we minimized the information by using isPicked instead of pickedCellIds, React will still re-render all 25 cells on each click (and each timer tick later on). However, the props minimization allows us to optimize the frequency with which that component re-renders itself by "memoizing" it. We can wrap a component with a React.memo call to memoize it. Memoizing comes with a small penalty because it requires comparing the props each time a component re-renders, but that penalty is usually less of an issue than actually re-rendering a component that didn’t need to be re-rendered, especially when there are some significant computations in these components.
What about gameStatus? Is that a minimum prop? Not really. When that value changes from NEW to CHALLENGE, we only need to re-render a few cells (to highlight them); we don’t need to re-render all cells.
We could make this better by not passing the gameStatus to Cell, and instead passing something like shouldBeHighlighted. However, I think it’s cleaner to just pass the gameStatus itself down to the Cell component. It’s really a core primitive value in each cell computation, and using it directly will make the code more readable. Besides, if we go the other way, more computational logic will need to exist in the GameSession component. I think all the logic about computing the status of a cell should be inside the Cell component. This is just my preference.
The practice I can get behind here is to try to only flow the minimum props that a component needs to re-render itself, but don’t sacrifice readability and responsibility management to be 100 percent exact about that.
I think that’s a good starting point. Let’s now compute the cellStatus value based on these three values.
We have the three variables that are directly responsible for the value of each cell status. We can come up with a rough description of the conditions around these variables that are responsible for what status a cell should have. It often helps to start with the pseudocode natural description of the computation we’re about to make. Here’s what I came up with based on the three points we analyzed when we came up with the cellStatus ENUM.
Code Listing 112
The cell-status starts as NORMAL If the game-status is NEW: Do nothing. All cells should be normal if the game-status is not NEW: if the cell is picked The cell-status should be either CORRECT or WRONG based on whether it is a challenge cell or not if the cell is not picked If it's a challenge cell and the game-status is CHALLENGE: The cell-status is HIGHLIGHT |
Technically, the "if the cell is picked" condition should also account for what game status we want the correct or wrong cell statuses to appear for. Do we want the green/pink highlights to stay when the game is won or lost? I think so. It is nice to have the players see the original challenge cells that they failed to guess. This is why I did not include a condition about game status in the is-picked branch. The only other game status value there is CHALLENGE, and we know for a fact that when the game has that status, no cell has been picked yet.
Here’s one way to translate the pseudocode in the Cell component:
Code Listing 113
const Cell = ({ width, gameStatus, isChallenge, isPicked }) => { let cellStatus = CellStatus.NORMAL; if (gameStatus !== GameStatus.NEW) { if (isPicked) { cellStatus = isChallenge ? CellStatus.CORRECT : CellStatus.WRONG; } else if ( isChallenge && (gameStatus === GameStatus.CHALLENGE || gameStatus === GameStatus.LOST) ) { cellStatus = CellStatus.HIGHLIGHT; } } return ( <div className="cell" style={{ width: ${width}%, backgroundColor: cellStatus }} /> ); }; |
Note how I added the LOST gameStatus case to show the cell as highlighted. There is no need to add the WON status there because all cells will be correctly picked in that case.
We can partially test this change by clicking the Start Game button. That click changes gameStatus to CHALLENGE, and the UI will show the six random blue cells. Render the UI a few times and click the Start Game button to verify that the random blue cells are different on each game render.
How can we test the rest of this logic? We have not implemented the "pick-cell" UI behavior yet. We don’t have a way to win or lose the game yet. We can use mock state values.
The state usually starts with empty values (like the empty pickedCellIds array). It is hard to design the UI without testing using actual values. Mock values come to the rescue.
For example, we can start the pickedCellIds with some mock values and start the gameStatus with a value of GameStatus.PLAYING:
Code Listing 114: Using mock state values
// In the Game component const [gameStatus, setGameStatus] = useState(GameStatus.PLAYING); const [pickedCellIds, setPickedCellIds] = useState([0, 1, 2, 22, 23, 24]); |
With these temporary changes in the initial values, and based on the randomly assigned challenge cells, the grid should show the first and last three cells as either pink or green. Print the challengeCellIds array to make sure the right cells are displayed green or pink:

Figure 19: Testing with mock initial values in the state
Note how cells #1 and #24 were green because they happened to be part of both the pickedCellIds and challengeCellIds when I took the screenshot. The other picked cells were pink because they were wrong picks for this mock state.
Tip: Change the gameStatus mock value to WON/LOST to verify that all the logic we have for cellStatus is working as expected.
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.
For example, another thing that needs to change in the UI based on the different states is the appearance of the button in the Footer component. The Start Game button should only show up when gameStatus is NEW. When the player wins or loses the game, a Play Again button should show up instead. While the player is playing the game, let’s just show the countdown value in the button area. A few if statements in the Footer component will do the trick:
Code Listing 115: The UI for the button area
const Footer = ({ gameStatus, countdown, startGame }) => { const buttonAreaContent = () => { if (gameStatus === GameStatus.NEW) { return <button onClick={startGame}>Start Game</button>; } if ( gameStatus === GameStatus.CHALLENGE || gameStatus === GameStatus.PLAYING ) { return countdown; } return <button onClick={() => {/* TODO */}}>Play Again</button>; }; return ( <> <div className="message">{Messages[gameStatus]}</div> <div className="button">{buttonAreaContent()}</div> </> ); }; // Then in GameSession, pass the newly-used countdown value <Footer gameStatus={gameStatus} countdown={countdown} startGame={() => setGameStatus(GameStatus.CHALLENGE)} /> |
Note a few things about what I did:
I think compromises like these are okay for prototyping, but you should get in the habit of trying to cover all the cases and think a tiny bit forward about the extendability of your code. I think I’d like the buttonAreaContent function better with a switch statement that does that:
Code Listing 116
const Footer = ({ gameStatus, countdown, startGame }) => { const buttonAreaContent = () => { switch(gameStatus) { case GameStatus.NEW: return <button onClick={startGame}>Start Game</button>; case GameStatus.CHALLENGE: // fall-through case GameStatus.PLAYING: return countdown; case GameStatus.WON: // fall-through case GameStatus.LOST: return <button onClick={() => {/* TODO */}}>Play Again</button>; } }; // ... }; |
You can test this new UI logic by just changing the initial value of gameStatus in the GameSession component.
Now all we need to do is implement the user interactions and have them change the state—then we’ll be done! React will always reflect the current values in data/state in the UI using the functions we already defined.
Note: The playground session for the code so far can be found here.
We implemented the first step of the Start Game button already: it changes the gameStatus value into CHALLENGE. What we need now is to change that value into PLAYING after three seconds (or whatever challengeSeconds value we end up using).
This is where we can use a timer object. The source of the behaviors that change the state is not always directly from the user (click or other events). Sometimes the behavior comes from a timer in the background.
We can start this timer in the same function that’s invoked when the Start Game button is clicked, the one where we inlined the call to setGameStatus. However, React has a different place for this logic. We can use a side-effect Hook (through the React.useEffect function).
Starting the timer is a side effect for changing the gameStatus value to CHALLENGE. Every time the gameStatus value changes to CHALLENGE, we need to start that timer. Every time the gameStatus is no longer CHALLENGE, we need to clear that timer if it’s still running.
The React.useEffect Hook function is designed to implement that exact logic. It takes a callback function and a list of dependencies:
Code Listing 117: The useEffect Hook
useEffect(() => { // do something when dep1 or dep2 changes }, [dep1, dep2]) |
During any rendering of the component, if the values of the dependencies are different from the last time useEffect was invoked, it’ll invoke its callback function. We can start the timer inside a side-effect Hook if we can determine that the gameStatus has changed to CHALLENGE. To do that, we can make gameStatus a dependency for our timer side-effect Hook.
Furthermore, if the component is re-rendered or removed from the DOM, a side effect can invoke a cleanup function. That cleanup function can be returned from the useEffect callback function. We should clean any timers we start in a side effect’s callback function in that side effect’s cleanup function.
Here’s what we need to change the gameStatus into PLAYING after three seconds using a side-effect Hook:
Code Listing 118: Using a timer in a side-effect Hook
// In the GameSession component useEffect(() => { let timerId; if (gameStatus === GameStatus.CHALLENGE) { timerId = setTimeout( () => setGameStatus(GameStatus.PLAYING), 1000 * challengeSeconds ); } return () => clearTimeout(timerId); }, [gameStatus, challengeSeconds]); |
Because gameStatus is a dependency, this useEffect Hook will invoke its callback function every time that value changes. When it changes into CHALLENGE, the if statement will become true and we’ll start a timer. In the cleanup function, we clear the timer.
For the sake of completeness, I also included the challengeSeconds as a dependency, although that value will not change during a single game. You should always include all the variables a side-effect Hook is using in its list of dependencies. You can even automate that in your editor using React ESLint tools like the eslint-plugin-react-hooks package.
Now the game will go into PLAYING mode three seconds after clicking the Start Game button, and the highlighting of challenge cells should go away. Test that.
When gameStatus changes into PLAYING, we need to start another timer. This time we need a timer that ticks 10 times repeatedly and modifies the countdown state element. This is where we can use a setInterval call. We’ll need to put this new logic in a side-effect Hook as well.
Because it’ll also depend on gameStatus, we can use the same Hook for both timers. They just have different branches that we can do through different if statements. Don’t forget that we need an exit condition for this new branch; we can check the countdown value to determine if the interval is to be stopped:
Here’s a way to do that:
Code Listing 119: Using an interval timer in a side-effect Hook
useEffect(() => { let timerId; // ... if (gameStatus === GameStatus.PLAYING) { timerId = setInterval(() => { if (countdown === 1) { clearTimeout(timerId); setGameStatus(GameStatus.LOST); } setCountdown(countdown - 1); }, 1000); } return () => clearTimeout(timerId); }, [gameStatus, challengeSeconds]); |
This will not work—the interval timer will continue to run. Test it and try to figure out why.
Because the code depended on the value of countdown and we have not included it as a dependency, the callback function will have a "stale" closure when the countdown value changes. That’s why you should always include all the dependencies. Adding countdown to the list of dependencies will solve the problem, but it’s not ideal.
By adding the countdown as a dependency, the Hook callback function (and its cleanup one) will be invoked every time the countdown value changes. That means every second, we will clear the interval timer and start a new one. We don’t even need an interval; we can replace setInterval with setTimeout and things will still work because of the recursive nature of repeated updates. A setTimeout changes the countdown value, React re-renders, the Hook’s callback function is invoked again, a setTimeout changes the countdown value, and so on.
The question is: How can we use a setInterval that updates the state then?
You’ll need the state-update logic to not depend on the current value of the state. We needed the countdown dependency in the first place because we’re decrementing the current value of that state element. However, all React state updater functions support their own callback form. You can pass a function to any updater function (like setCountdown), and that function will be called with the current value of the state as its argument. Whatever that function returns becomes the new value for that state element.
For this example, we just need to move the logic that depends on countdown into the setCountdown callback function, and we can keep the setInterval use that way:
Code Listing 120: Using a callback function in a state updater function
useEffect(() => { let timerId; // ... if (gameStatus === GameStatus.PLAYING) { timerId = setInterval(() => { setCountdown(countdown => { if (countdown === 1) { clearTimeout(timerId); setGameStatus(GameStatus.LOST); } return countdown - 1; }); }, 1000); } return () => clearTimeout(timerId); }, [challengeSeconds, gameStatus]); |
This version does not depend on the value of countdown at all. React will make the current value of countdown available to setCountdown when it invokes it. This will work fine.
This callback version of React state updater functions is much better because it allows you to optimize any code that updates the state. I’d go as far as saying you should always use the callback version if your state-update logic depends on the current value of the state (like the decrement logic we had).
Tip: Whenever you create a timeout or interval timers in a React component, do not forget to clear them when they’re no longer needed (for example, when the component gets removed from the DOM). You can use the useEffect cleanup function or the componentWillUnmount lifecycle method in class components.
Note: The playground session for the code so far can be found here.
The core user interaction in this game, and the one that’ll be responsible for the remaining computations we need to make, is the pick-cell behavior.
When a player clicks a cell two things are affected:
Because these states are managed in the GameSession component, we need to place the pick-cell behavior on that level.
Let’s start by defining an empty pickCell function in GameSession and pass that function to Cell so that we can make the Cell component click handler. We’ll wire it to receive the ID of the cell being clicked:
Code Listing 121
// In the GameSession component: const pickCell = cellId => { }; // ... <Cell key={cellId} width={cellWidth} gameStatus={gameStatus} isChallenge={challengeCellIds.includes(cellId)} isPicked={pickedCellIds.includes(cellId)} onClick={() => pickCell(cellId)} /> // Then in the Cell component: const Cell = ({ width, gameStatus, isChallenge, isPicked, onClick, }) => { // ... return ( <div className="cell" style={{ width: ${width}%, backgroundColor: cellStatus }} onClick={onClick} /> ); }, [challengeSeconds, gameStatus]); |
The first change this new function is going to make is straightforward. We can use array destructuring to append the new clicked cellId to the pickedCellIds array:
Code Listing 122
// In the GameSession component: const pickCell = cellId => { if (gameStatus === GameStatus.PLAYING) { setPickedCellIds(pickedCellIds => { if (pickedCellIds.includes(cellId)) { return pickedCellIds; } return [...pickedCellIds, cellId]; }); } }; |
Note a few things about this code:
With this code, you can now partially play the game. You can pick cells and see them change to green or pink, but you cannot win or lose the game through picking (the game will be over after 10 seconds through the timer code).
Where can we implement the win/lose game status logic? We can put it in the pickCell function itself! After all, we need to check if the game is WON or LOST after each pick. There is a problem though: this logic will depend on the new value for pickedCellIds after each pick. We need to do it after the setPickedCellIds call. All React state updater functions are asynchronous. We can’t do anything after them directly.
We could work around this problem by using the new pickedCellIds array before we use it with setPickedCellIds. Here’s a version of the code to do that:
Code Listing 123: Using the "state to be" in computations
// In the GameSession component: const pickCell = cellId => { if (gameStatus === GameStatus.PLAYING) { setPickedCellIds(pickedCellIds => { if (pickedCellIds.includes(cellId)) { return pickedCellIds; } const newPickedCellIds = [...pickedCellIds, cellId]; const [correctPicks, wrongPicks] = utils.arrayCrossCounts( newPickedCellIds, challengeCellIds ); if (correctPicks === challengeSize) { setGameStatus(GameStatus.WON); } if (wrongPicks === maxWrongAttempts) { setGameStatus(GameStatus.LOST); } return newPickedCellIds; }); } }; |
The math logic to compute the new gameStatus values is not really important, but I am basically using the utils.arrayCrossCounts array to count how many correctPicks and wrongPicks the player has made so far (including the current pick), and then compare these numbers with challengeSize/maxWrongAttempts to determine the new gameStatus value.
By inserting this logic right before the return value, I get to use the value of "the state to be." Go ahead and try to play now—you can win and lose the game.
What’s wrong with this code though? Nothing, right? Let’s ship it!
While the code in Code Listing 123 works just fine, putting the logic to update gameStatus in pickCell itself makes the function less readable and harder to reason with. The pickCell function should just pick a cell!
Where are we supposed to put the win/lose logic then?
The fact that we need to invoke the win/lose logic is a side effect to updating the pickedCellIds array. Every time we update the pickedCellIds array, we need to invoke this side effect. Although it is really "internal" to React and has nothing to do with anything external to it, we can still use a side-effect Hook just to separate these two different concerns.
Revert pickCell to its first version and move the win/lose logic to its own new useEffect Hook:
Code Listing 124: Using a side-effect Hook to compute a new state
// In the GameSession component: React.useEffect(() => { const [correctPicks, wrongPicks] = utils.arrayCrossCounts( pickedCellIds, challengeCellIds ); if (correctPicks === challengeSize) { setGameStatus(GameStatus.WON); } if (wrongPicks === maxWrongAttempts) { setGameStatus(GameStatus.LOST); } }, [pickedCellIds]); |
This is a bit cleaner. There is no mix of concerns or confusing new versus old picked cells concepts. Each time the pickedCellIds array is different, the useEffect callback function will be invoked because we wired pickedCellIds as a dependency for it. There is no need to do any cleanup for this side-effect Hook because it isn’t really a full side effect that depends on something external to React. It is only used to manage internal concerns.
Note: The playground session for the code so far can be found here.
The final behavior we need to implement for this game is the Play Again button. To reset a game session, we can simply re-initialize the state elements with their first initial values. For example, we could have a resetGame function like this:
Code Listing 125: Resetting a component state
// In the GameSession component: const resetGame = () => { setGameStatus(GameStatus.NEW); setPickedCellIds([]); setCountdown(playSeconds); }; // .. <Footer gameStatus={gameStatus} countdown={countdown} startGame={() => setGameStatus(GameStatus.CHALLENGE)} resetGame={resetGame} /> // In the Footer component: const Footer = ({ gameStatus, countdown, startGame, resetGame }) => { const buttonAreaContent = () => { switch(gameStatus) { // ... case GameStatus.WON: // fall-through case GameStatus.LOST: return <button onClick={resetGame}>Play Again</button>; } }; // ... }; |
This method will partially work. It’ll even reset the timers because they are cleared in the useEffect cleanup function that gets invoked before the new game is rendered. However, this method has two problems. Can you identify them?
The first problem is a small one. This method introduces a bit of a code duplication problem. If we’re to add more elements to the state of this component (for example, to display the number of current wrong attempts), then we’ll have to remember to reset this new state element in resetGame. This is probably not a big deal for a small app like this one.
The second problem is a bigger one. I saved this solution here for you to test it. Play a few times and try to identify the problem.
Between game resets, the challenge cells remain the same. We’re only resetting the state of the GameSession component. We’re not resetting anything in the GameGenerator component which is responsible for the static data elements that the GameSession component uses. In addition to resetting the state of the GameSession component, we need the GameGenerator to re-render itself, compute a new set of random challenge cells, and render a fresh version of the GameSession component.
React offers a trick to combine both actions needed to reset a game session. We can re-render the GameGenerator component (by changing its state) and use a different identity value for the GameSession component through the special "key" prop. That’ll make React render a brand new GameSession instance (using initial state values).
When we use the key attribute for a component, React uses it as the unique identity of that component. Say we rendered a component with key="X", and then later re-rendered the exact same component (same props and state) but with key="Y". React will assume that these two are different elements. It’ll completely remove the X component from the DOM (which is known as "unmounting"), and then it’ll mount a new version of it as Y, although nothing else has changed in X besides its key.
This can be used to reset any stateful component. We just give each game instance a key and change that key to reset it. We’ll need to do that in the GameGenerator component and since we need that component to re-render, we can use a stateful element for the value of this new key prop.
To make this change, remove the resetGame function from the GameSession component and instead make it receive a prop named resetGame. The GameGenerator component will now be in charge of resetting a game:
Code Listing 126
const Game = ({ cellIds, challengeCellIds, cellWidth, challengeSize, challengeSeconds, playSeconds, maxWrongAttempts, autoStart, resetGame, }) => { // Remove the resetGame function }; |
Next, make the following changes to the GameGenerator component:
Code Listing 127: Changing a component identity
const GameGenerator = () => { const [gameId, setGameId] = useState(1); // .. return ( <GameSession key={gameId} cellIds={cellIds} challengeCellIds={challengeCellIds} cellWidth={cellWidth} challengeSize={challengeSize} challengeSeconds={3} playSeconds={10} maxWrongAttempts={3} resetGame={() => setGameId(gameId => gameId + 1)} /> ); }; |
That’s it—now the game will reset properly. All computed data elements in GameGenerator will be re-computed with the gameId state change. All props passed to the GameSession component will have a new value. The state of the GameSession component will be re-initialized because it’s now a brand-new element in the main tree (because of the new key value).
It would be nice, however, to auto-start the second game session (and all sessions after that) instead of having the player click the Start Game button again after clicking Play Again.
Try to do that on your own first.
The GameGenerator component is the one that knows if a game was the first one, or if it was generated through a resetGame action. In fact, that condition is simply gameId > 1. However, the GameSession component is the one responsible for starting the game (through its gameStatus value). One way to solve this issue is by passing an autoStart prop to GameSession and using it to initialize the gameStatus variable as either NEW or CHALLENGE right away:
Code Listing 128: Controlling state initial value with a prop
const Game = ({ cellIds, challengeCellIds, cellWidth, challengeSize, challengeSeconds, playSeconds, maxWrongAttempts, autoStart, resetGame, }) => { const [gameStatus, setGameStatus] = useState( autoStart ? GameStatus.CHALLENGE : GameStatus.NEW ); // ... }; const GameGenerator = () => { const [gameId, setGameId] = useState(1); // .. return ( <Game key={gameId} cellIds={cellIds} challengeCellIds={challengeCellIds} cellWidth={cellWidth} challengeSize={challengeSize} challengeSeconds={3} playSeconds={10} maxWrongAttempts={3} autoStart={gameId > 1} resetGame={() => setGameId(gameId => gameId + 1)} /> ); }; |
The "Play Again" action will now render a brand-new game, and it will directly be in its CHALLENGE mode.
This game is now feature-complete. However, before we conclude, there is one more trick that I’d like you to be aware of. One of the best things about React Hooks is that we can extract related logic into a custom function. Let’s do that.
Note: The playground session for the code so far can be found here.
The logic to have a gameId that starts with 1, an autoStart game flag, and a resetGame function that increments gameId is all related. Right now, it’s within the GameGenerator component mixed with the other logic about cellIds and challengeCellIds.
If we decide later to change the values of gameId to be string-based instead of an incrementing number, we’ll have to modify the GameGenerator component. Ideally, this logic should be extracted into its own unit, and the GameGenerator can just use that unit. This is exactly like extracting a piece of code into a function and invoking that function, but this particular piece of code is stateful. The useState call Hooks into React internals to associate a component with a state element.
Luckily, React supports extracting stateful code into a function. You can extract any Hook calls including useState and useEffect into a function (which is known as a custom Hook) and React will continue to associate the Hook calls with the component that invoked the custom Hook function.
Name your custom Hook function useXYZ. This way, linting tools can treat it as yet another React Hook and provide helpful hints and warnings about it. This is not a regular function—it’s a function that contains stateful logic for a component.
I’ll name this new custom Hook function useGameId. You can make it receive any arguments you want, and make it return any data in any shape or form. It does not have to be similar to useState (or other React Hooks), and we don’t need to pass any argument to it in this case. I’ll also make it return the three elements related to the game ID concept: the ID itself, a way to tell if this is the first ID, and a way to generate a new ID:
Code Listing 129: Using a custom Hook function
const useGameId = () => { const [gameId, setGameId] = useState(1); return { gameId, isNewGame: gameId === 1, renewGame: () => setGameId(gameId => gameId + 1), }; }; const GameGenerator = () => { const { gameId, isNewGame, renewGame } = useGameId(); // ... return ( <Game key={gameId} cellIds={cellIds} challengeCellIds={challengeCellIds} cellWidth={cellWidth} challengeSize={challengeSize} challengeSeconds={3} playSeconds={10} maxWrongAttempts={3} autoStart={!isNewGame} resetGame={renewGame} /> ); }; |
Note how I opted to use different names for elements of the custom Hook. This custom Hook can be reusable across components. Different game apps like this one can use the same useGameId custom Hook function to provide identity for their components, offer a way to do something for a new or subsequent game, and provide a method to renew the gameId identity.
Tip: Identify other areas in the code where a custom Hook can improve the separation of concerns in the code. Create and use a custom Hook and make sure things still work after you do.
Note: The playground session for the final code can be found here.
Let’s say you took this version to the client and they loved it, but of course they want more. Here are two major features that I’ll leave you with as a bonus challenge on this game: