This content originally appeared on DEV Community and was authored by Jhey Tompkins
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')
We could make this smaller, like so:
render(<h1>{`Time: ${Date.now()}`}</h1>, document.getElementById('app'))
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()}`)
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>
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>
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>
}
Use a class
class App extends React.Component {
render() {
return <h1>{`Time: ${Date.now()}`}</h1>
}
}
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>
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'))
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>
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!"
}
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>
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}/>
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>
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'))
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>
)
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'))
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>
)
}
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
. Fragment
s 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>
}
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
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.
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
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>
}
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])
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])
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 Mole
s.
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>
)
}
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".
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 Mole
s 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, 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()
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) =>
<Mole key={id} onWhack={onWhack} />
)}
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)
}
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} />
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])
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>
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.
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>
)
}
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 Mole
s, 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 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)
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
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>
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 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]
}
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.
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.
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.
- React documentation
- “Making setInterval Declarative with React Hooks” – Dan Abramov
- “How to fetch data with React Hooks” – Robin Wieruch
- “When to useMemo and useCallback” – Kent C Dodds
Stay Awesome! ʕ •ᴥ•ʔ
This content originally appeared on DEV Community and was authored by Jhey Tompkins
Jhey Tompkins | Sciencx (2021-10-22T16:55:20+00:00) Get Started with React by Building a Whac-A-Mole Game. Retrieved from https://www.scien.cx/2021/10/22/get-started-with-react-by-building-a-whac-a-mole-game-2/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.