Get Started With React By Building A Whac-A-Mole Game

I’ve been working with React since ~v0.12 was released. (2014! Wow, where did the time go?) It’s changed a lot. I recall certain “Aha” moments along the way. One thing that’s remained is the mindset for using it. We think about things in a different way as opposed to working with the DOM direct.

For me, my learning style is to get something up and running as fast as I can. Then I explore deeper areas of the docs and everything included whenever necessary. Learn by doing, having fun, and pushing things!

Aim

The aim here is to show you enough React to cover some of those “Aha” moments. Leaving you curious enough to dig into things yourself and create your own apps.
I recommend checking out the docs for anything you want to dig into. I won’t be duplicating them.

Please note that you can find all examples in CodePen, but you can also jump to my Github repo for a fully working game.

First App

You can bootstrap a React app in various ways. Below is an example:

import React from 'https://cdn.skypack.dev/react'
import { render } from 'https://cdn.skypack.dev/react-dom' const App = () => <h1>{`Time: ${Date.now()}`}</h1> render(<App/>, document.getElementById('app')

Starting Point

We’ve learned how to make a component and we can roughly gauge what we need.

import React, { Fragment } from 'https://cdn.skypack.dev/react'
import { render } from 'https://cdn.skypack.dev/react-dom' const Moles = ({ children }) => <div>{children}</div>
const Mole = () => <button>Mole</button>
const Timer = () => <div>Time: 00:00</div>
const Score = () => <div>Score: 0</div> const Game = () => ( <Fragment> <h1>Whac-A-Mole</h1> <button>Start/Stop</button> <Score/> <Timer/> <Moles> <Mole/> <Mole/> <Mole/> <Mole/> <Mole/> </Moles> </Fragment>
) render(<Game/>, document.getElementById('app'))

Starting/Stopping

Before we do anything, we need to be able to start and stop the game. Starting the game will trigger elements like the timer and moles to come to life. This is where we can introduce conditional rendering.

const Game = () => { const [playing, setPlaying] = useState(false) return ( <Fragment> {!playing && <h1>Whac-A-Mole</h1>} <button onClick={() => setPlaying(!playing)}> {playing ? 'Stop' : 'Start'} </button> {playing && ( <Fragment> <Score /> <Timer /> <Moles> <Mole /> <Mole /> <Mole /> <Mole /> <Mole /> </Moles> </Fragment> )} </Fragment> )
}

We have a state variable of playing and we use that to render elements that we need. In JSX, we can use a condition with && to render something if the condition is true. Here we say to render the board and its content if we are playing. This also affects the button text where we can use a ternary.

Open the demo at this link and set the extension to highlight renders. Next, you’ll see that the timer renders as time changes, but when we whack a mole, all components re-render.

Loops in JSX

You might be thinking that the way we’re rendering our Moles is inefficient. And you’d be right to think that! There’s an opportunity for us here to render these in a loop.

With JSX, we tend to use Array.map 99% of the time to render a collection of things. For example:

const USERS = [ { id: 1, name: 'Sally' }, { id: 2, name: 'Jack' },
]
const App = () => ( <ul> {USERS.map(({ id, name }) => <li key={id}>{name}</li>)} </ul>
)

The alternative would be to generate the content in a for loop and then render the return from a function.

return ( <ul>{getLoopContent(DATA)}</ul>
)

What’s that key attribute for? That helps React determine what changes need to render. If you can use a unique identifier, then do so! As a last resort, use the index of the item in a collection. (Read the docs on lists for more.)

For our example, we don’t have any data to work with. If you need to generate a collection of things, then here’s a trick you can use:

new Array(NUMBER_OF_THINGS).fill().map()

This could work for you in some scenarios.

return ( <Fragment> <h1>Whac-A-Mole</h1> <button onClick={() => setPlaying(!playing)}>{playing ? 'Stop' : 'Start'}</button> {playing && <Board> <Score value={score} /> <Timer time={TIME_LIMIT} onEnd={() => console.info('Ended')}/> {new Array(5).fill().map((_, id) => <Mole key={id} onWhack={onWhack} /> )} </Board> } </Fragment>
)

Or, if you want a persistent collection, you could use something like uuid:

import { v4 as uuid } from 'https://cdn.skypack.dev/uuid'
const MOLE_COLLECTION = new Array(5).fill().map(() => uuid()) // In our JSX
{MOLE_COLLECTION.map((id) => )}

Ending Game

We can only end our game with the Start button. When we do end it, the score remains when we start again. The onEnd for our Timer also does nothing yet.

We’re going to bring in a third-party solution to make our moles bob up and down. This is an example of how to bring in third-party solutions that work with the DOM. In most cases, we use refs to grab DOM elements, and then we use our solution within an effect.

We’re going to use GreenSock(GSAP) to make our moles bob. We won’t dig into the GSAP APIs today, but if you have any questions about what they’re doing, please ask me!

Here’s an updated Mole with GSAP:

import gsap from 'https://cdn.skypack.dev/gsap' const Mole = ({ onWhack }) => { const buttonRef = useRef(null) useEffect(() => { gsap.set(buttonRef.current, { yPercent: 100 }) gsap.to(buttonRef.current, { yPercent: 0, yoyo: true, repeat: -1, }) }, []) return ( <div className="mole-hole"> <button className="mole" ref={buttonRef} onClick={() => onWhack(MOLE_SCORE)}> Mole </button> </div> )
}

We’ve added a wrapper to the button which allows us to show/hide the Mole, and we’ve also given our button a ref. Using an effect, we can create a tween (GSAP animation) that moves the button up and down.

You’ll also notice that we’re using className which is the attribute equal to class in JSX to apply class names. Why don’t we use the className with GSAP? Because if we have many elements with that className, our effect will try to use them all. This is why useRef is a great choice to stick with.

See the Pen 8. Moving Moles by @jh3y.

Awesome, now we have bobbing Moles, and our game is complete from a functional sense. They all move exactly the same which isn’t ideal. They should operate at different speeds. The points scored should also reduce the longer it takes for a Mole to get whacked.

Our Mole’s internal logic can deal with how scoring and speeds get updated. Passing the initial speed, delay, and points in as props will make for a more flexible component.

<Mole key={index} onWhack={onWhack} points={MOLE_SCORE} delay={0} speed={2} />

Now, for a breakdown of our Mole logic.

Let’s start with how our points will reduce over time. This could be a good candidate for a ref. We have something that doesn’t affect render whose value could get lost in a closure. We create our animation in an effect and it’s never recreated. On each repeat of our animation, we want to decrease the points value by a multiplier. The points value can have a minimum value defined by a pointsMin prop.

const bobRef = useRef(null)
const pointsRef = useRef(points) useEffect(() => { bobRef.current = gsap.to(buttonRef.current, { yPercent: -100, duration: speed, yoyo: true, repeat: -1, delay: delay, repeatDelay: delay, onRepeat: () => { pointsRef.current = Math.floor( Math.max(pointsRef.current * POINTS_MULTIPLIER, pointsMin) ) }, }) return () => { bobRef.current.kill() }
}, [delay, pointsMin, speed])

We’re also creating a ref to keep a reference for our GSAP animation. We will use this when the Mole gets whacked. Note how we also return a function that kills the animation on unmount. If we don’t kill the animation on unmount, the repeat code will keep firing.

See the Pen 9. Score Reduction by @jh3y.

What will happen when a mole gets whacked? We need a new state for that.

const [whacked, setWhacked] = useState(false)

And instead of using the onWhack prop in the onClick of our button, we can create a new function whack. This will set whacked to true and call onWhack with the current pointsRef value.

const whack = () => { setWhacked(true) onWhack(pointsRef.current)
} return ( <div className="mole-hole"> <button className="mole" ref={buttonRef} onClick={whack}> Mole </button> </div>
)

The last thing to do is respond to the whacked state in an effect with useEffect. Using the dependency array, we can make sure we only run the effect when whacked changes. If whacked is true, we reset the points, pause the animation, and animate the Mole underground. Once underground, we wait for a random delay before restarting the animation. The animation will start speedier using timescale and we set whacked back to false.

useEffect(() => { if (whacked) { pointsRef.current = points bobRef.current.pause() gsap.to(buttonRef.current, { yPercent: 100, duration: 0.1, onComplete: () => { gsap.delayedCall(gsap.utils.random(1, 3), () => { setWhacked(false) bobRef.current .restart() .timeScale(bobRef.current.timeScale() * TIME_MULTIPLIER) }) }, }) }
}, [whacked])

That gives us:

See the Pen 10. React to Whacks by @jh3y.

The last thing to do is pass props to our Mole instances that will make them behave differently. But, how we generate these props could cause an issue.

<div className="moles"> {new Array(MOLES).fill().map((_, id) => ( <Mole key={id} onWhack={onWhack} speed={gsap.utils.random(0.5, 1)} delay={gsap.utils.random(0.5, 4)} points={MOLE_SCORE} /> ))}
</div>

This would cause an issue because the props would change on every render as we generate the moles. A better solution could be to generate a new Mole array each time we start the game and iterate over that. This way, we can keep the game random without causing issues.

const generateMoles = () => new Array(MOLES).fill().map(() => ({ speed: gsap.utils.random(0.5, 1), delay: gsap.utils.random(0.5, 4), points: MOLE_SCORE
}))
// Create state for moles
const [moles, setMoles] = useState(generateMoles())
// Update moles on game start
const startGame = () => { setScore(0) setMoles(generateMoles()) setPlaying(true) setFinished(false)
}
// Destructure mole objects as props
<div className="moles"> {moles.map(({speed, delay, points}, id) => ( <Mole key={id} onWhack={onWhack} speed={speed} delay={delay} points={points} /> ))}
</div>

And here’s the result! I’ve gone ahead and added some styling along with a few varieties of moles for our buttons.

See the Pen 11. Functioning Whac-a-Mole by @jh3y.

We now have a fully working “Whac-a-Mole” game built in React. It took us less than 200 lines of code. At this stage, you can take it away and make it your own. Style it how you like, add new features, and so on. Or you can stick around and we can put together some extras!

Tracking The Highest Score

We have a working “Whac-A-Mole”, but how can we keep track of our highest achieved score? We could use an effect to write our score to localStorage every time the game ends. But, what if persisting things was a common need. We could create a custom hook called usePersistentState. This could be a wrapper around useState that reads/writes to localStorage.

 const usePersistentState = (key, initialValue) => { const [state, setState] = useState( window.localStorage.getItem(key) ? JSON.parse(window.localStorage.getItem(key)) : initialValue ) useEffect(() => { window.localStorage.setItem(key, state) }, [key, state]) return [state, setState]
}

And then we can use that in our game:

const [highScore, setHighScore] = usePersistentState('whac-high-score', 0)

We use it exactly the same as useState. And we can hook into onWhack to set a new high score during the game when appropriate:

const endGame = points => { if (score > highScore) setHighScore(score) // play fanfare!
}

How might we be able to tell if our game result is a new high score? Another piece of state? Most likely.

See the Pen 12. Tracking High Score by @jh3y.

Whimsical Touches

At this stage, we’ve covered everything we need to. Even how to make your own custom hook. Feel free to go off and make this your own.

Sticking around? Let’s create another custom hook for adding audio to our game:

const useAudio = (src, volume = 1) => { const [audio, setAudio] = useState(null) useEffect(() => { const AUDIO = new Audio(src) AUDIO.volume = volume setAudio(AUDIO) }, [src]) return { play: () => audio.play(), pause: () => audio.pause(), stop: () => { audio.pause() audio.currentTime = 0 }, }
}

This is a rudimentary hook implementation for playing audio. We provide an audio src and then we get back the API to play it. We can add noise when we “whac” a mole. Then the decision will be, is this part of Mole? Is it something we pass to Mole? Is it something we invoke in onWhack ?

These are the types of decisions that come up in component-driven development. We need to keep portability in mind. Also, what would happen if we wanted to mute the audio? How could we globally do that? It might make more sense as a first approach to control the audio within the Game component:

// Inside Game
const { play: playAudio } = useAudio('/audio/some-audio.mp3')
const onWhack = () => { playAudio() setScore(score + points)
}

It’s all about design and decisions. If we bring in lots of audio, renaming the play variable could get tedious. Returning an Array from our hook-like useState would allow us to name the variable whatever we want. But, it also might be hard to remember which index of the Array accounts for which API method.

See the Pen 13. Squeaky Moles by @jh3y.

That’s It!

More than enough to get you started on your React journey, and we got to make something interesting. We sure did cover a lot:

  • Creating an app,
  • JSX,
  • Components and props,
  • Creating timers,
  • Using refs,
  • Creating custom hooks.

We made a game! And now you can use your new skills to add new features or make it your own.

Where did I take it? At the time of writing, it’s at this stage so far:

See the Pen Whac-a-Mole w/ React && GSAP by @jh3y.

Where To Go Next!

I hope building “Whac-a-Mole” has motivated you to start your React journey. Where next? Well, here are some links to resources to check out if you’re looking to dig in more — some of which are ones I found useful along the way.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

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