DEV Community

Cover image for Get Started with React by Building a Whac-A-Mole Game
Jhey Tompkins
Jhey Tompkins

Posted on • Originally published at jhey.dev on

Get Started with React by Building a Whac-A-Mole Game

Want to get started with React but struggling to find a good place to start? This article should have you covered. We'll focus on some of the main concepts of React and then we'll be building a game from scratch! We assume that you have a working knowledge of JavaScript — ah, and if you're here for the game, please scroll down.

I've been working with React for a long time. Since ~v0.12 (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, etc. when 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 — this is pretty much all you need to create your first React app (besides the HTML) to get started.

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')
Enter fullscreen mode Exit fullscreen mode

We could make this smaller, like so:

render(<h1>{`Time: ${Date.now()}`}</h1>, document.getElementById('app'))
Enter fullscreen mode Exit fullscreen mode

In the first version, App is a component. But, this example tells React DOM to render an element instead of a component. Elements are the HTML elements we see in both examples. What makes a component, is a function returning those elements.

Before we get started with components, what's the deal with this "HTML in JS"?

JSX

That "HTML in JS" is JSX. You can read all about JSX in the React documentation. The gist? A syntax extension to JavaScript that allows us to write HTML in JavaScript. It's like a templating language with full access to JavaScript powers. It's actually an abstraction on an underlying API. Why do we use it? For most, it's easier to follow and comprehend than the equal.

React.createElement('h1', null, `Time: ${Date.now()}`)
Enter fullscreen mode Exit fullscreen mode

The thing to take on board with JSX is that this is how you put things in the DOM 99% of the time with React. And it's also how we bind event handling a lot of the time. That other 1% is a little out of scope for this article. But, sometimes we want to render elements outside the realms of our React application. We can do this using React DOM's Portal. We can also get direct access to the DOM within the component lifecycle(coming up).

Attributes in JSX are camelCase. For example, onclick becomes onClick. There are some special cases such as class which becomes className. Also, attributes such as style now accept an Object instead of a string.

const style = { backgroundColor: 'red' }
<div className="awesome-class" style={style}>Cool</div>
Enter fullscreen mode Exit fullscreen mode

Note: You can check out all the differences in attributes here.

Rendering

How do we get our JSX into the DOM? We need to inject it. In most cases, our apps have a single point of entry. And if we are using React, we use React DOM to insert an element/component at that point. You could use JSX without React though. As we mentioned, it's a syntax extension. You could change how JSX gets interpreted by Babel and have it pump out something different.

Everything within becomes managed by React. This can yield certain performance benefits when we are modifying the DOM a lot. This is because React makes use of a Virtual DOM. Making DOM updates isn't slow by any means. But, it's the impact it has within the browser that can impact performance. Each time we update the DOM, browsers need to calculate the rendering changes that need to take place. That can be expensive. Using the Virtual DOM, these DOM updates get kept in memory and synced with the browser DOM in batches when required.

There's nothing to stop us from having many apps on a page or having only part of a page managed by React.

Take this example. The same app rendered twice between some regular HTML. Our React app renders the current time using Date.now.

const App = () => <h1>{`Time: ${Date.now()}`}</h1>
Enter fullscreen mode Exit fullscreen mode

For this example, we're rendering the app twice between some regular HTML. We should see the title "Many React Apps", followed by some text. Then the first rendering of our app appears, followed by some text and then the second rendering of our app.

For a deeper dive into rendering, check out the docs.

Components && Props

This is one of the biggest parts of React to grok. Components are reusable blocks of UI. But underneath, it's all functions. Components are functions whose arguments we refer to as props. And we can use those "props" to determine what a component should render. Props are "read-only" and you can pass anything in a prop. Even other components. Anything within the tags of a component we access via a special prop, children.

Components are functions that return elements. If we don’t want to show anything, return null.

We can write components in a variety of ways. But, it's all the same result.

Use a function

function App() {
  return <h1>{`Time: ${Date.now()}`}</h1>
}
Enter fullscreen mode Exit fullscreen mode

Use a class

class App extends React.Component {
  render() {
    return <h1>{`Time: ${Date.now()}`}</h1>
  }
}
Enter fullscreen mode Exit fullscreen mode

Before the release of hooks(coming up), we used class-based components a lot. We needed them for state and accessing the component API. But, with hooks, the use of class-based components has petered out a bit. In general, we always opt for function-based components now. This has various benefits. For one, it requires less code to achieve the same result. Hooks also make it easier to share and reuse logic between components. Also, classes can be confusing. They need the developer to have an understanding of bindings and context.

We'll be using function-based and you'll notice we used a different style for our App component.

const App = () => <h1>{`Time: ${Date.now()}`}</h1>
Enter fullscreen mode Exit fullscreen mode

That's valid. The main thing is that our component returns what we want to render. In this case, a single element that is a h1 displaying the current time. If we don't need to write return, etc. then don’t. But, it's all preference. And different projects may adopt different styles.

What if we updated our multi-app example to accept props and we extract the h1 as a component?

const Message = ({ message }) => <h1>{message}</h1>
const App = ({ message }) => <Message message={message} />
render(<App message={`Time: ${Date.now()}`}/>, document.getElementById('app'))
Enter fullscreen mode Exit fullscreen mode

That works and now we can change the message prop on App and we'd get different messages rendered. We could've made the component Time. But, creating a Message component implies many opportunities to reuse our component. This is the biggest thing about React. It’s about making decisions around architecture/design.

What if we forget to pass the prop to our component? We could provide a default value. Some ways we could do that.

const Message = ({message = "You forgot me!"}) => <h1>{message}</h1>
Enter fullscreen mode Exit fullscreen mode

Or by specifying defaultProps on our component. We can also provide propTypes which is something I'd recommend having a look at. It provides a way to type check props on our components.

Message.defaultProps = {
  message: "You forgot me!"
}
Enter fullscreen mode Exit fullscreen mode

We can access props in different ways. We've used ES6 conveniences to destructure props. But, our Message component could also look like this and work the same.

const Message = (props) => <h1>{props.message}</h1>
Enter fullscreen mode Exit fullscreen mode

Props are an object passed to the component. We can read them any way we like.

Our App component could even be this

const App = (props) => <Message {...props}/>
Enter fullscreen mode Exit fullscreen mode

It would yield the same result. We refer to this as "Prop spreading". It's better to be explicit with what we pass through though.

We could also pass the message as a child.

const Message = ({ children }) => <h1>{children}</h1>
const App = ({ message }) => <Message>{message}</Message>
Enter fullscreen mode Exit fullscreen mode

Then we refer to the message via the special children prop.

How about taking it further and doing something like have our App pass a message to a component that is also a prop.

const Time = ({ children }) => <h1>{`Time: ${children}`}</h1>

const App = ({ message, messageRenderer: Renderer }) => <Renderer>{message}</Renderer>

render(<App message={`${Date.now()}`} messageRenderer={Time} />, document.getElementById('app'))
Enter fullscreen mode Exit fullscreen mode

In this example, we create two apps and one renders the time and another a message. Note how we rename the messageRenderer prop to Renderer in the destructure? React won't see anything starting with a lowercase letter as a component. That’s because anything starting in lowercase is seen as an element. It would render it as <messageRenderer>. It's rare that we'll use this pattern but it's a way to show how anything can be a prop and you can do what you want with it.

One thing to make clear is that anything passed as a prop needs processing by the component. For example, want to pass styles to a component, you need to read them and apply them to whatever is being rendered.

Don't be afraid to experiment with different things. Try different patterns and practice. The skill of determining what should be a component comes through practice. In some cases, it's obvious, and in others, you might realize it later and refactor.

A common example would be the layout for an application. Think at a high level what that might look like. A layout with children that comprises of a header, footer, some main content. How might that look? It could look like this.

const Layout = ({ children }) => (
  <div className="layout">
    <Header/>
    <main>{children}</main>
    <Footer/>
  </div>
)
Enter fullscreen mode Exit fullscreen mode

It's all about building blocks. Think of it like LEGO for apps.

In fact, one thing I'd advocate is getting familiar with Storybook as soon as possible (I'll create content on this if people would like to see it). Component-driven development isn't unique to React, we see it in other frameworks too. Shifting your mindset to think this way will help a lot.

Making Changes

Up until now, we've only dealt with static rendering. Nothing changes. The biggest thing to take on board for learning React is how React works. We need to understand that components can have state. And we must understand and respect that state drives everything. Our elements react to state changes. And React will only re-render where necessary.

Data flow is unidirectional too. Like a waterfall, state changes flow down the UI hierarchy. Components don't care about where the data comes from. For example, a component may want to pass state to a child through props. And that change may trigger an update to the child component. Or, components may choose to manage their own internal state which isn't shared.

These are all design decisions that get easier the more you work with React. The main thing to remember is how unidirectional this flow is. To trigger changes higher up, it either needs to happen via events or some other means passed by props.

Let's create an example.

import React, { useEffect, useRef, useState } from 'https://cdn.skypack.dev/react'
import { render } from 'https://cdn.skypack.dev/react-dom'

const Time = () => {
  const [time, setTime] = useState(Date.now())
  const timer = useRef(null)
  useEffect(() => {
    timer.current = setInterval(() => setTime(Date.now()), 1000)
    return () => clearInterval(timer.current)
  }, [])
  return <h1>{`Time: ${time}`}</h1>
}

const App = () => <Time/>

render(<App/>, document.getElementById('app'))
Enter fullscreen mode Exit fullscreen mode

There is a fair bit to digest there. But, here we introduce the use of "Hooks". We are using "useEffect", "useRef", and "useState". These are utility functions that give us access to the component API.

If you check the example, the time is updating every second or 1000ms. And that's driven by the fact we update the time which is a piece of state. We are doing this within a setInterval. Note how we don't change time directly. State variables are treated as immutable. We do it through the setTime method we receive from invoking useState. Every time the state updates, our component re-renders if that state is part of the render. useState always returns a state variable and a way to update that piece of state. The argument passed is the initial value for that piece of state.

We use useEffect to hook into the component lifecycle for events such as state changes. Components mount when they're inserted into the DOM. And they get unmounted when they're removed from the DOM. To hook into these lifecycle stages, we use effects. And we can return a function within that effect that will fire when the component gets unmounted. The second parameter of useEffect determines when the effect should run. We refer to it as the dependency array. Any listed items that change will trigger the effect to run. No second parameter means the effect will run on every render. And an empty array means the effect will only run on the first render. This array will usually contain state variables or props.

We are using an effect to both setup and tear down our timer when the component mounts and unmounts.

We use a ref to reference that timer. A ref provides a way to keep reference to things that don’t trigger rendering. We don't need to use state for the timer. It doesn't affect rendering. But, we need to keep a reference to it so we can clear it on unmount.

Want to dig into hooks a bit before moving on? I wrote an article before about them – "React Hooks in 5 Minutes". And there's also great info in the React docs.

Our Time component has its own internal state that triggers renders. But, what if we wanted to change the interval length? We could manage that from above in our App component.

const App = () => {
  const [interval, updateInterval] = useState(1000)
  return (
    <Fragment>
      <Time interval={interval} />
      <h2>{`Interval: ${interval}`}</h2>
      <input type="range" min="1" value={interval} max="10000" onChange={e => updateInterval(e.target.value)}/>
    </Fragment>
  )
}
Enter fullscreen mode Exit fullscreen mode

Our new interval value is being stored in the state of App. And it dictates the rate at which the Time component updates.

The Fragment component is a special component we have access to through React. In React, a component must return a single child or null. We can't return adjacent elements. But, sometimes we don't want to wrap our content in a div. Fragments allow us to avoid wrapper elements whilst keeping React happy.

You'll also notice our first event bind happening there. We use onChange as an attribute of the input to update the interval.

The updated interval is then passed to Time and the change of interval triggers our effect to run. This is because the second parameter of our useEffect hook now contains interval.

const Time = ({ interval }) => {
  const [time, setTime] = useState(Date.now())
  const timer = useRef(null)
  useEffect(() => {
    timer.current = setInterval(() => setTime(Date.now()), interval)
    return () => clearInterval(timer.current)
  }, [interval])
  return <h1>{`Time: ${time}`}</h1>
}
Enter fullscreen mode Exit fullscreen mode

Have a play with the demo and see the changes!


I recommend visiting the React documentation if you want to dig into some of these concepts more. But, we've seen enough React to get started making something fun! Let's do it!

Whac-A-Mole React Game

Are you ready? We'll be creating our very own "Whac a Mole" with React!. The well-known game is basic in theory but throws up some interesting challenges to build. The important part here is how we're using React. I'll gloss over applying styles and making it pretty. That's your job! Although, I'm happy to take any questions on that.

Also, this game will not be "polished". But, it works. You can go and make it your own! Add your own features, etc.

Design

Let's start by thinking about what we've got to make. What components we may need etc.

  • Start/Stop Game
  • Timer
  • Keeping Score
  • Layout
  • Mole Component Whac a Mole Sketch

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'))
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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.

Timer

Let's get the timer running. By default, we're going to set a time limit of 30000ms. And we can declare this as a constant outside of our React components.

const TIME_LIMIT = 30000
Enter fullscreen mode Exit fullscreen mode

Declaring constants in one place is a good habit to pick up. Anything that can be used to configure your app can be co-located in one place.

Our Timer component only cares about three things.

  • The time it's counting down;
  • At what interval it's going to update;
  • What it does when it ends.

A first attempt might look like this.

const Timer = ({ time, interval = 1000, onEnd }) => {
  const [internalTime, setInternalTime] = useState(time)
  const timerRef = useRef(time)
  useEffect(() => {
    if (internalTime === 0 && onEnd) onEnd()
  }, [internalTime, onEnd])
  useEffect(() => {
    timerRef.current = setInterval(
      () => setInternalTime(internalTime - interval),
      interval
    )
    return () => {
      clearInterval(timerRef.current)
    }
  }, [])
  return <span>{`Time: ${internalTime}`}</span>
}
Enter fullscreen mode Exit fullscreen mode

But, it only updates once?

We're using the same interval technique we did before. But, the issue is we're using state in our interval callback. And this is our first "gotcha". Because we have an empty dependency array for our effect, it only runs once. The closure for setInterval uses the value of internalTime from the first render. This is an interesting problem and makes us think about how we approach things.

Note: I highly recommend reading this article by Dan Abramov that digs into timers and how to get around this problem. It's a worthwhile read and provides a deeper understanding. One issue is that empty dependency arrays can often introduce bugs in our React code. There's also an eslint plugin I'd recommend using to help point these out. The React docs also highlight the potential risks of using the empty dependency array.

One way to fix our Timer would be to update the dependency array for the effect. This would mean that our timerRef would get updated every interval. However, it introduces the issue of drifting accuracy.

useEffect(() => {
  timerRef.current = setInterval(
  () => setInternalTime(internalTime - interval),
    interval
  )
  return () => {
  clearInterval(timerRef.current)
  }
}, [internalTime, interval])
Enter fullscreen mode Exit fullscreen mode

If you check this demo, it has the same Timer twice with different intervals and logs the drift to the developer console. A smaller interval or longer time equals a bigger drift.

We can use a ref to solve our problem. We can use it to track the internalTime and avoid running the effect every interval.

const timeRef = useRef(time)
useEffect(() => {
  timerRef.current = setInterval(
    () => setInternalTime((timeRef.current -= interval)),
    interval
  )
  return () => {
    clearInterval(timerRef.current)
  }
}, [interval])
Enter fullscreen mode Exit fullscreen mode

And this reduces the drift significantly with smaller intervals too. Timers are sort of an edge case. But, it's a great example to think about how we use hooks in React. It's an example that's stuck with me and helped me understand the “Why?”.

Update the render to divide the time by 1000 and append an s and we have a seconds timer.

This timer is still rudimentary. It will drift over time. For our game, it'll be fine. If you want to dig into accurate counters, this is a great video on creating accurate timers with JavaScript.

Scoring

Let's make it possible to update the score. How do we score? Whacking a mole! In our case, that means clicking a button. For now, let's give each mole a score of 100. And we can pass an onWhack callback to our Moles.

const MOLE_SCORE = 100

const Mole = ({ onWhack }) => (
  <button onClick={() => onWhack(MOLE_SCORE)}>Mole</button>
)

const Score = ({ value }) => <div>{`Score: ${value}`}</div>

const Game = () => {
  const [playing, setPlaying] = useState(false)
  const [score, setScore] = useState(0)

  const onWhack = points => setScore(score + points)

  return (
    <Fragment>
      {!playing && <h1>Whac a Mole</h1>}
      <button onClick={() => setPlaying(!playing)}>{playing ? 'Stop' : 'Start'}</button>
      {playing &&
        <Fragment>
          <Score value={score} />
          <Timer
            time={TIME_LIMIT}
            onEnd={() => setPlaying(false)}
          />
          <Moles>
            <Mole onWhack={onWhack} />
            <Mole onWhack={onWhack} />
            <Mole onWhack={onWhack} />
            <Mole onWhack={onWhack} />
            <Mole onWhack={onWhack} />
          </Moles>
        </Fragment>
      }
    </Fragment>
  )
}
Enter fullscreen mode Exit fullscreen mode

Note how the onWhack callback gets passed to each Mole. And that the callback updates our score state. These updates will trigger a render.

This is a good time to install the React Developer Tools extension in your browser. There is a neat feature that will highlight component renders in the DOM. Open the "Components" tab in Dev Tools and hit the settings cog. Select "Highlight updates when components render".

Setting up React DevTools

If you open our demo at this link and set the extension to highlight renders. Then you will 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 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>
)
Enter fullscreen mode Exit fullscreen mode

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>
)
Enter fullscreen mode Exit fullscreen mode

What's that key attribute for? That helps React determine what changes need to render. If you can use a unique identifier, do! 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. There's a trick you can use.

new Array(NUMBER_OF_THINGS).fill().map()
Enter fullscreen mode Exit fullscreen mode

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>
)
Enter fullscreen mode Exit fullscreen mode

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) =>
  <Mole key={id} onWhack={onWhack} />
)}
Enter fullscreen mode Exit fullscreen mode

Ending Game

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

What we need is a 3rd state where we aren't playing but we have finished. In more complex applications, I'd recommend reaching for XState or using reducers. But, for our app, we can introduce a new state variable, finished. When the state is !playing and finished, we can display the score, reset the timer, and give the option to restart.

We need to put our logic caps on now. If we end the game, then instead of toggling playing, we need to also toggle finished. We could create an endGame and startGame function.

const endGame = () => {
  setPlaying(false)
  setFinished(true)
}

const startGame = () => {
  setScore(0)
  setPlaying(true)
  setFinished(false)
}
Enter fullscreen mode Exit fullscreen mode

When we start a game, we reset the score and put the game into the playing state. This triggers the playing UI to render. When we end the game, we set finished to true. The reason we don't reset the score is so we can show it as a result.

And, when our Timer ends, it should invoke that same function.

<Timer time={TIME_LIMIT} onEnd={endGame} />
Enter fullscreen mode Exit fullscreen mode

It can do that within an effect. If the internalTime hits 0, then unmount and invoke onEnd.

useEffect(() => {
  if (internalTime === 0 && onEnd) {
    onEnd()
  }
}, [internalTime, onEnd])
Enter fullscreen mode Exit fullscreen mode

We can shuffle our UI rendering to render 3 states:

  • Fresh
  • Playing
  • Finished
<Fragment>
  {!playing && !finished &&
    <Fragment>
      <h1>Whac a Mole</h1>
      <button onClick={startGame}>Start Game</button>
    </Fragment>
  }
  {playing &&
    <Fragment>
      <button
        className="end-game"
        onClick={endGame}
        >
        End Game
      </button>
      <Score value={score} />
      <Timer
        time={TIME_LIMIT}
        onEnd={endGame}
      />
      <Moles>
        {new Array(NUMBER_OF_MOLES).fill().map((_, index) => (
          <Mole key={index} onWhack={onWhack} />
        ))}
      </Moles>
    </Fragment>
  }
  {finished &&
    <Fragment>
      <Score value={score} />
      <button onClick={startGame}>Play Again</button>
    </Fragment>
  }
</Fragment>
Enter fullscreen mode Exit fullscreen mode

And now we have a functioning game minus moving moles.

Note how we’ve reused the Score component. Was there an opportunity there to not repeat Score? Could you put it in its own conditional? Or does it need to appear there in the DOM. This will come down to your design.

Might you end up with a more generic component to cover it? These are the questions to keep asking. The goal is to keep a separation of concerns with your components. But, you also want to keep portability in mind.

Moles

Moles are the centerpiece of our game. They don't care about the rest of the app. But, they'll give you their score onWhack. This emphasises portability.

We aren't digging into styling in this "Guide". But, for our Mole, we can create a container with overflow: hidden that our Mole(button) moves in and out of. The default position of our Mole will be out of view.

Mole Design

We're going to bring in a 3rd party solution to make our Moles bob up and down. This is an example of how to bring in 3rd 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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, 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.

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} />
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

We're also creating a ref to keep a reference of 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.

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

const [whacked, setWhacked] = useState(false)
Enter fullscreen mode Exit fullscreen mode

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>
)
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

That gives us

The last thing to do is pass props to our Mole instances that will make them behave different. 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

And here's the result! I’ve gone ahead and added some styling along with an image of a Mole for our buttons.

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, etc. Or stick around and we can put together some extras.

Tracking High Score

We have a working "Whac a Mole". But, how can we keep track of our high 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]
}
Enter fullscreen mode Exit fullscreen mode

And then we can use that in our game.

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

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!
}
Enter fullscreen mode Exit fullscreen mode

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

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
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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.

That's It!

More than enough to get you started on your React journey. And we got to make something interesting.

We covered 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? It's at this stage so far.

Where to Go Next!

I hope building “Whac-a-Mole” has motivated you to start your React journey. Where next?

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.

Stay Awesome! ʕ •ᴥ•ʔ

Top comments (3)

Collapse
 
ben profile image
Ben Halpern

wac-a-mole

Collapse
 
nickytonline profile image
Nick Taylor

This is epic Jhey! Someone should make Quac-a-mole.

Guac-a-mole game

Collapse
 
jh3y profile image
Jhey Tompkins

Guess I'd better skin this one! 😅