The Memory Challenge Game
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:

Here’s how this game works:
-
The first UI will be an empty grid (no colors) and a "Start Game" button. Clicking that button starts the game. This means the game will have different statuses and we need to manage that with a React state element (because we need the UI to change).
-
When the game starts, a few cells on the grid will be highlighted (with a light blue background) for 3 seconds and then the grid resets back to no colors. The player is then given 10 second to recall the locations of the blue cells to win. This will require the use of timers in the code and that’ll teach us about side effect hooks in React.
-
During the 10 seconds play time, the player can do up to 2 wrong picks. The game will be over at 3 wrong picks or if the 10 seconds timer runs out. A wrong pick is shown in the UI with pink background while a correct one is shown with a light green background. We’ll need to figure out what minimum information we need to place on React’s state to satisfy all these UI changes.
-
When the game is done (either with a win or lose status), a "Play Again" button shows up and that button will reset everything and start a new game. This will teach us how to properly reset a stateful component which has side effects.
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.
Initial markup and style
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 what 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 3x3
grid:
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);
*, *: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:
-
The template HTML was rendered with a single top-level React component (which I named
Game
) -
I intentionally kept the list of cells flat to simplify the code. I used the power of styling to display the list of flat
divs
as a grid. The alternative is to use a list of lists and convert that into rows and columns, which would have complicated the code a bit. I love how CSS can sometimes greatly simplify UI logic. If something can be adequately done with CSS, you should consider that. -
I used React’s special
style
prop for the width of each cell. This width value will later be driven based on a dynamic size for the grid and using React’sstyle
object will make that change easier.
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 do that when we start using them).
// 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 2 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.
Extracting components
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.
-
We extract components to make part of the tree define data requirements or behavior on its own and take responsibility of it. In this game, all grid cells will be clickable to count towards a guess and their click events should trigger a UI update. This is a good indicator that we should create a component to represent a single grid cell. I’ll name it
Cell
-
We extract components to make the code more readable. A component with too much JSX is harder to reason with and maintain. To demonstrate an example of this, I’ll make the footer part (which holds the game message an a button) into a component. I’ll name it
Footer
.
We extract components for other reasons too. For example, when there is duplication of sub-trees or 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
:
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 the behavior each grid cell will do for 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 3 components.
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. 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 pick, the better. An increment could be anything. There is no right answer. You could focus on defining a click handler on |
Making the grid dynamic
Imagine needing to pass a prop value to each Cell
element. Right now we would need to do that in 9 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 was to have a fixed grid that is always 3x3
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 9 (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 for 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 by just changing its props.
// In the ReactDOM.render call <Game gridSize={5} />
Note again how I used {5} and not "5" because I need the Game component to 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 cell to highlight as challenge? The maximum wrong attempts allowed? The number of seconds to highlight the challenge cells? The number of seconds that’ll time-out the game? All of these should really be made into variables in the code. I’ll make them all as props to the Game
component.
// 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 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:
const Game = ({ gridSize }) => { const cellIds = utils.createArray(gridSize * gridSize); // ... };
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.
const Game = ({ gridSize }) => { // ... <div className="grid"> {cellIds.map(cellId => <Cell key={cellId} /> )} </div> // ... };
Note how I used the values in the cellIds array as keys for Cell . This not the same as using the loop index as 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, that app will render a grid of 5x5
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 and it’s simply equal to 100/gridSize
. We can pass that value as a prop to the Cell
component.
We could do something like:
<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:
const Game = ({ gridSize }) => { // .. const cellWidth = 100 / gridSize; // ... <div className="grid"> {cellIds.map(cellId => <Cell key={cellId} width={cellWidth} /> )} // ... };
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% 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%
:
const Cell = ({ width }) => { return ( <div className="cell" style={{ width: `${width}%` }} /> ); };
All the changes we did 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 and 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.
What’s a good next increment now?
Let’s think about what we need to place on the state of this game.
Designing data and state elements
When the "Start Game" button is clicked, 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 3 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 2 components. I’ll name this state element gameStatus
and it will have 5 different values. Let’s give each one a label:
-
A game starts with a
gameStatus
ofNEW
which is before the "Start Game" button is clicked. -
When the "Start Game" button is clicked, the
gameStatus
becomesCHALLENGE
. This is when we highlight the blue challenge cells. -
After a delay of
challengeSeconds
thegameStatus
becomesPLAYING
. This is when the player can actually recall the challenge cells (which should not be highlighted at that point). -
If the player wins the game, the
gameStatus
becomesWON
. If the player loses the game, thegameStatus
becomesLOST
.
We can place this gameStatus
variable on the Game
component’s state with a useState
call. The initial value is NEW
:
const Game = ({ gridSize }) => { const [gameStatus, setGameStatus] = useState('NEW'); // ... }
Then when the "Start Game" button is clicked we can use setGameStatus
to change the gameStatus
variable into 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:
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 ENUMs
Using strings as labels has 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 amount of 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 that 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:
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:
-
A cell starts with a status of
NORMAL
. This is when it’s white. -
If the cell is a challenge cell and the game is in the
CHALLENGE
phase, the cell will have a status ofHIGHLIGHT
. This is when it’s highlighted in blue. -
When the player picks a cell, its status will become either
CORRECT
orWRONG
based on whether it’s a challenge cell or not. ACORRECT
cells will be highlighted with a light-green color and aWRONG
one with pink.
Since these cell statuses directly drive the color values, I’ll make their ENUM value the colors themselves:
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:
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 they won or lost:
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 lookup the game message in Footer
. We could alternatively lookup 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
.
const Footer = ({ gameStatus, startGame }) => { // ... <div className="message">{Messages[gameStatus]}</div> // .. }; const Game = ({ gridSize }) => { // .. <Footer gameStatus={gameStatus} startGame={() => setGameStatus(GameStatus.CHALLENGE)} /> // ... };
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.
|
Identifying computed values
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 (which is 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 this challengeCellIds
array will change during a game session or not.
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 plain-old variable in the Game
component:
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); // ... };
Always place the state element first and then introduce any "computed" ones after that. |
The Game
component is now aware 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!
To see the problem I am talking about, log the value of challengeCellIds
after the line that defines it:
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 on that button.

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).
So why is this happening? Didn’t we define challengeCellIds
using the const
keyword? How is it changing when we click that button?
Wrapping stateful components
You need to keep in mind that each time you invoke a useState
updater function, which is what we’re doing in the onClick
handler for 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 a new cellIds
and cellWidth
variables too).
These 3 variables should not be recreated. 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 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.
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
into GameSession
and keep the console.log
line in there for testing. Create a GameGenerator
component and move the computing of all 3 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:
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:
-
I moved the fixed values of
gridSize
andchallengeSize
into theGameGenerator
component (instead of having them as props forGameSession
). This was just to simplify the new code but it’ll also be a first step for you to implement some of the bonus challenges at the end of the chapter. -
The 3 variables we moved out of the
GameSession
component and into theGameGenerator
component are now passed toGameSession
as props using the same names. Nothing else needs to change in theGameSession
component’s render logic. -
The
GameGenerator
component has no state of its own (yet). You don’t need to place the state of your app on the top-level component. For this particular case, keeping the state in theGameSession
component (which is now a subtree) allowed us to "cache" the app-global variables in the parent tree. -
Since the
gridSize
is no longer needed in theGameSession
component, we don’t need to keep it as a prop on it. All the other props I kept are needed. We’re just not using them all yet. For example, thechallengeSize
is needed to determine aGameStatus.WON
state and themaxWrongAttempts
is needed to determine aGameStatus.LOST
state. The "seconds" variables are needed to manage the timers. I destructured all these props in theGameSession
component.
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.

Determining what to make stateful
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 a 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 picked 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:
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?
No. Both of these cases are also computable. With a challengeSize
of 6 and maxWrongAttempts
of 3, after every guess if we have 6 correctly guessed cells, the game is won. If we have 3 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 status requires some array math and placing them on the state is a form of simply caching this computation.
Ideally, caching the WON/LOST |
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
down to 0 in the UI. We’ll need a state element to hold these countdown values. I’ll name countdown
. It’s initial value is the playSeconds
prop:
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 state 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.
Completing the UI as a function of state
Now that we identified and designed all the static and dynamic data elements this game is going to need, 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 pickedCellIds
value. Each cell status depends on all 3 variables.
Practically adopting the minimum props concept
To keep the code simple, I’ll compute each cell status in the Cell
component itself. Does this mean we need to flow down gameStatus
, challengeCellIds
, and pickedCellIds
from the GameSession
component to the Cell
component?
// 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 need to know whether it is a challenge cell and a picked cell. What we need are flags like isPicked
and isChallenge
and we can use a simple Array.includes
to compute them:
// 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?
No. Only one cell needs to be re-rendered. That one cell that will change its isPicked
value from false to true.
With every prop you pass to a child component comes great responsibility!
React will re-render all components in a sub-tree when the parent component re-renders. So, although we minimized the information by using |
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
down to Cell
and instead pass 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 down the minimum props that a component needs to re-render itself but don’t sacrifice readability and responsibility managements to be 100% exact about that.
I think that’s a good starting point. Let’s now compute the cellStatus
value based on these 3 values.
Computing values before rendering
We have the 3 variables that are directly responsible for the value of each cell status. We can come up with 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 pseudo-code natural description of the computation we’re about to make. Here’s what I came up with based on the 3 points we analyzed when we came up with the cellStatus
ENUM.
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 note if the cell is not picked If it's a challenge cell and the game-status is CHALLENGE: The cell-status is HIGHLIGHT
Technically, if "the cell is picked" condition should also account for what game-status we want the correct/wrong cell statuses to appear. 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 pseudo-code in the Cell
component:
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 on the "Start Game" button. That click changes gameStatus
into CHALLENGE
and the UI will show the 6 random blue cells. Render the UI a few times and click on that 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!
Using 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 to the rescue.
For example, we can start the pickedCellIds
with some mock values and start the gameStatus
with a value of GameStatus.PLAYING
:
// 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:

Note how cells #1
and #24
were green because they happen to be part of both the pickedCellIds
and challengeCellIds
when I took the screenshot. The other picked cells were pink because they are wrong picks for this mock state.
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 with 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:
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 used a function to compute the content of the button area. This approach has a few advantages over just inlining the code in the component function itself (like what we did for
cellStatus
in theCell
component). This is really a style preference but the use of early returns and the isolation of that logic into a single unit (that, for example, is testable on its own if need be) are among the reasons I like this approach better. You could also extract that part itself into its own component really. A component is just a function after all. -
I rendered the "Play Again" button but we have not implemented its behavior yet. I left a TODO comment in there instead. If you need to inline comments in JSX, that’s how you do it.
-
I omitted the conditions to render the "Play Again" button because they’re all that’s remaining (after the first 2 checks that cover 3 statuses). However, this means if another
GameStatus
value is introduced, this code will stop behaving right for all the cases. Shall we fix that?
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 extendibility of your code. I think I’d like the buttonAreaContent
function better with a switch
statement that does that:
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 is to 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 that we already defined.
Implementing behaviors to change the state
Using a timeout side effect
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 3 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 change 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:
useEffect(() => { // Do something after each render // but only if dep1 or dep2 changed }, [dep1, dep2]);
After 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 3 seconds using 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 after 3 seconds of clicking the "Start Game" button 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:
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 update. 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:
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 depend on current value of the state (like the decrement logic we had).
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.
|
Implementing computations that depend on a state update
The core user-interaction in this game and the one that’ll be responsible for the rest of computations we need to make is the pick-cell behavior.
When a player clicks a cell 2 things are affected:
-
We will always need to change the
pickedCellIds
array and add the clicked cell to it. -
We might need to change the
gameStatus
value (whenmaxWrongAttempts
is reached).
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:
// 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}
/>
);
The first change this new function is going to do is straightforward. We can use array destructuring to append the new clicked cellId
to the pickedCellIds
array:
// 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:
-
I made it check the
gameStatus
first. A player can only pick a cell when the game mode isPLAYING
. -
I used the callback version of the
setPickedCellIds
function although I did not need to. Because I am using the current value ofpickedCellIds
(to append to it), it’s always a good practice to use the callback version rather than relying on thepickedCellIds
value exposed through function closures. -
I added a check to make sure that we do not pick a cell twice. This is important for the logic to determine if a game is
WON
orLOST
.
With this code, you can now partially play the game. You can pick cells and see them change to green/pink but you cannot win/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:
// 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), 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? SHIP IT!
Using side effects to separate concerns
While the code above 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?
Think about it. 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 2 different concerns.
Revert pickCell
to its first version and move the win/lose logic to its own new useEffect
hook:
// 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 vs.
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.
Resetting a React component
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 resetGame
function like this:
// 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 get invoked before the new game is rendered. However, this method has 2 problems. Can you identify them?
-
The first problem is a small one. This method introduces a bit of code duplication problem. If we’re to add more elements to the state of this component (for example, 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 at jsdrops.com/rs3.9 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.
Changing a component’s identity
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-render the exact same component (same props and state) but with key="Y"
. React will assume that these 2 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 it 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:
const Game = ({ cellIds, challengeCellIds, cellWidth, challengeSize, challengeSeconds, playSeconds, maxWrongAttempts, autoStart, resetGame, }) => { // Remove the resetGame function };
Then make the following changes to the GameGenerator
component:
-
Define a
gameId
state variables and pass it as the key for React to identify aGameSession
instance. -
Pass
resetGame
to theGameSession
component as a function that’ll change thegameId
state variable.
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 be 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).
I 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.
Controlling state initial value with a prop
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 use it to initialize the gameStatus
variable as either NEW
or CHALLENGE
right away:
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 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.
Using custom hooks
The logic to have a gameId
that starts with 1, have 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 decided later to change the values of gameId
to be string-based instead of 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 hooks calls including useState
and useEffect
into a function (which is known as a custom hook) and React will continue to associate the hooks calls with the component that invoked the custom hook function.
Name your custom hook function as 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 contain 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 3 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:
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 re-usable 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.
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. |
Bonus challenges
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:
-
Track scores: A perfect score of 3 happens when the player guesses all correct cells without any wrong attempts. If they make 1 wrong attempt the score is 2 and with 2 wrong attempts the score is 1. Make the score time-aware. If the player finishes the game in the first 5 seconds, double their score. Display the total score in the UI somewhere. When the player wins the game again, add the new score to the total score.
-
Make it harder: When the player hits "Play Again", make the grid bigger,
6x6
, then7x7
, and so on. Also, increment the challenge size with each new game.