Daniel Eden, Designer

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:

  1. Is mutable (can be changed easily just by assigning it a new value)
  2. 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.