Building a Coin-Tossing Simulator with React, Hooks, and Vercel: Part 3
It’s a new year, and although it’s a bit late, I’m pleased to get stuck into the third and final part of our journey with React. If you remember from part one, we started this exercise with the idea of tossing a coin forever and seeing how it averages out to a roughly equal number of heads vs. tails; but there’s still one thing missing from our coin-tossing simulation:
There will be stretches of perfect fullness, consisting of a long run of 1’s. There will be stretches of perfect emptiness, consisting of a long run of 0’s.
How are we to track these stretches? That’s what we’ll solve for in this final installment, before deploying our simulator onto the web with a simple command.
To track the record number of consecutive 1’s and 0’s, we’re going to use many of the same tools we’ve used, with the introduction of one new function.
import React from "react"
const { useEffect, useRef, useState } = React
function App() {
const [side, setSide] = useState(1)
const prevSide = useRef(side)
const [heads, setHeads] = useState(0)
const [tails, setTails] = useState(0)
const [isPaused, setIsPaused] = useState(false)
const [currentStretch, setCurrentStretch] = useState(0)
const [headsRecord, setHeadsRecord] = useState(0)
const [tailsRecord, setTailsRecord] = useState(0)
const tossed = heads + tails
// ...
We’ve added a few lines to App.js
. First, from React, we want to grab one new
function: useRef
. In React, a “ref” is a variable that:
- Is mutable (can be changed easily just by assigning it a new value)
- Persists throughout a component’s lifecycle (meaning it sticks around for as long as the component is rendered)
Usually, refs are used to allow developers to interact with DOM elements directly (as opposed to managing them through React’s component lifecycle), but in this case, we’re going to use it to store the previous result of the coin toss.
We’ll initialise prevSide
to equal side
, and we’ll refer to it later in the
tossCoin
function:
const [side, setSide] = useState(1)
const prevSide = useRef(side)
We also need a few variables to keep track of the current stretch of consecutive
heads or tails, as well as the record number of consecutive heads and tails
respectively. We can use good old useState
once more, and we’ll initialise
each of the values as 0
since on the first render, no records have been set.
const [currentStretch, setCurrentStretch] = useState(0)
const [headsRecord, setHeadsRecord] = useState(0)
const [tailsRecord, setTailsRecord] = useState(0)
Interlude: What’s Happening When We Call useState
?
We’ve used useState
quite a lot in our app so far—7 times to my count—but you
might be wondering exactly what useState
does, and why we don’t store our
state variables some other way, like using a regular old var
or in an Object.
Imagine you’re at a restaurant with a bunch of your friends, giving your order
to the waitstaff. In this analogy, React is the waitstaff and your orders are
calls to useState
.
The waitstaff is going to write your orders down in an ordered list, which they’ll read back to you later. Half way through your order, one of your friends might say “actually, I changed my mind—I’d like the steak instead of the salad.”
If we were to write the code for this scenario, it might look like this:
const [order, changeOrder] = useState("salad")
And later, when our friend changes their order:
changeOrder("steak")
When changeOrder
is called (your friend changes their mind), the waitstaff
(React) goes through their list of orders, finds the one that your friend
originally made, scratches it out, and changes it to the new order.
This is a simplification, but not a big one: state variables, when managed by
useState
, is just a list/array of values that changes over time. What’s
important in the management of this list of values is that React is in control
of when the values change. Giving control of the values over to React helps to
ensure that our application stays fast and only updates when necessary.
Let’s get back to our app. Now that we’ve got variables for our records, and a
reference letting us know which side of the coin appeared on the previous
toss, we need to update the tossCoin
function to properly update those
variables. Here’s what our tossCoin
function will look like after these
updates:
const tossCoin = () => {
const landedOn = Math.round(Math.random())
if (landedOn !== prevSide.current) {
switch (landedOn) {
case 0:
setTailsRecord(Math.max(currentStretch, tailsRecord))
break
case 1:
default:
setHeadsRecord(Math.max(currentStretch, headsRecord))
break
}
setCurrentStretch(1)
prevSide.current = landedOn
} else {
setCurrentStretch(currentStretch + 1)
}
if (landedOn === 1) {
setHeads(heads + 1)
} else {
setTails(tails + 1)
}
setSide(landedOn)
}
Note that the whole block is inside an if
statement. We only run this code if
the most recent coin toss has a different result than the previous one:
basically if a streak of either heads or tails ends.
I’m introducing a new syntax you may not be familiar with called a switch
statement. A switch statement lets us check the value of a variable, and
execute code depending on that value. It’s a bit like an if
statement, but
saves us from a lot of else if
checks.
In this case, we’re checking which side the coin landed on, and updating the corresponding record. Here’s that switch statement again, with comments to help clarify what’s happening:
// Given the `landedOn` value...
switch (landedOn) {
// If the coin landed on tails,
case 0:
// Update the tails record to `currentStretch` or
// `tailsRecord`, whichever is highest
setTailsRecord(Math.max(currentStretch, tailsRecord))
// `break` the switch and go back outside the switch
// statement, since our condition was met.
break
// If the coin landed on heads,
case 1:
// Or if we encounter a different value (this shouldn't
// happen, but it's important to make our code safe
// against bugs)
default:
// Update the tails record to `currentStretch` or
// `headsRecord`, whichever is highest
setHeadsRecord(Math.max(currentStretch, headsRecord))
// `break` the switch and go back outside the switch
// statement, since our condition was met.
break
}
Whew! Take a minute to re-read that code and make sure you understand what’s happening.
Immediately after the switch statement, we reset the current record. Because the
record began when the current coin face is different to the previous coin face,
the current face’s record starts with 1
. We also want to set the prevSide
reference to the current coin face.
setCurrentStretch(1)
prevSide.current = landedOn
One more thing: if the current coin face is the same as the previous one, we
should increment the currentStretch
value! Putting the lot together (and
skipping past the switch statement since we looked at it in detail already)
looks like this:
// If the streak has ended...
if (landedOn !== prevSide.current) {
// The `switch` statement goes here and updates the
// record for heads or tails
// Reset the current record
setCurrentStretch(1)
// And update the reference for future checks
prevSide.current = landedOn
} else {
// Otherwise, the streak is still going!
setCurrentStretch(currentStretch + 1)
}
Now that we’ve added all the logic we need to keep track of the streaks of consecutive heads or tails, all we need to do is render the output.
Find the return
value for our component and add the code to render the
records:
return (
<div>
{/* Code from parts 1 & 2... */}
<h2>Records</h2>
<ul>
<li>Heads: {headsRecord}</li>
<li>Tails: {tailsRecord}</li>
</ul>
</div>
)
And that’s it! After making these updates, you should now see a changing record of the number of consecutive heads and tails. You might notice that the records quickly change from 0 to about 5 or 6, and then much more slowly increase to 10 or so, before slowing down completely and only increasing every now and then.
This isn’t surprising! Try it yourself at home by tossing a coin: you’ll find that consecutive runs of heads or tails are quite rare, but if you were to toss a coin many thousands or millions of times, eventually you might find incredibly long streaks of heads or tails. (Luckily, we built this app to simulate those coin tosses happening much more quickly!)
You can see the full App.js
code up to this point
here.
The Final Step: Deploying Our App
Once you’ve built a React application, you’ll want to deploy it for the world to see. Thankfully, nowadays deploying a React app is made easy by services that let you do it for free and quickly!
One of those services is called Vercel. Assuming you
have npm
or yarn
installed, as in
Part 1, you can install the
vercel
command line tool like so:
npm install --global vercel
Or for Yarn:
yarn global add vercel
Once installed, from inside your React application’s directory, simply type
vercel
and hit enter. Vercel will show you your app being uploaded to their
servers and then give you a URL where you—or anyone across the globe—can see
your app running live!
You may have noticed that I’ve been using Vercel for all the live instances of my application throughout this series of posts. Here’s the latest version of our coin toss application running on Vercel.
Wrapping Up
What a journey we’ve been on! We learned a bit about React, its syntax, and how
to leverage its component lifecycle with useState
and useEffect
; we’ve
looked at JavaScript features like object destructuring and switch statements;
and we’ve deployed a React application to a web server for the world to see!
I hope these posts have been interesting—even helpful—for people curious about React, and that you find ways to take some of the concepts introduced here and build some fun things of your own.
As a bonus, I updated the coin toss application one more time with some style changes and a slider to let people control how often our virtual coin is tossed. You can always find the latest version of the application here and see its code here.