Skip to content

Blog Claps, and lessons on Hooks 🎉


Last week we finished migrating our React Training Blog off Medium and into here. As I was doing the migration I thought, Michael and Ryan sure do have a lot of "claps" for their posts, would be a shame to lose those.

So I did what any developer would do, I re-created it.


This article tells a little bit of a story about how I got to the final code. If you want to, you can just skip to the finished code.

First, my requirements were:

  1. Let the user click as many times as they want, similar to Medium (although I found out later that Medium might allow only 50 claps per person, but whatever ours is unlimited)
  2. Don't require authentication like Medium does. It's just an extra barrier to prevent someone from clapping but also we don't have other features yet that require it so let's just make this lightweight for now.
  3. Debounce the network requests so when someone is rapidly clicking the clap button, we don't send off a new request for each click.

#1 and #2 are somewhat related. If we had authentication or some sort of IP tracking we could limit how much people can clap, but this is "just for fun" and isn't meant to be taken too seriously as a metric for anything.

The <BlogClaps> Component

This is approximately what I came up with at first, but there's some problems we'll discuss:

import React, { useState } from 'react'
import { debounce } from 'lodash'

function BlogClaps() {
  const [claps, setClaps] = useState(0)
  const [queueClaps, setQueueClaps] = useState(0)

  const saveClaps = debounce(claps => {
    // This is where the network request would be, when it
    // resolves we would set `claps` to be whatever the network
    // says it is, and then reset `queueClaps`. But for now
    // we're skipping that part and just setting claps manually:
    setClaps(claps)
    setQueueClaps(0)
  }, 700) // Debounce for 700ms

  function clap() {
    setQueueClaps(queueClaps + 1)
    saveClaps(queueClaps + 1)
  }

  return (
    <div>
      <button onClick={clap}>
        Clap
      </button>{' '}
      {queueClaps}/{claps}
    </div>
  )
}

render(<BlogClaps />, document.getElementById('root'))

For now we'll be skipping the network part and just get the basic functionality.

If you're not super comfortable with hooks yet (and I'm still getting there, it takes some getting used to), I recommend this article by Dan Abramov. It's long, but it will most likely double your React Hooks game. Make sure you are somewhat comfortable before continuing this article because we're about to get into the weeds.

Debouncing

In case you're not familiar, debouncing is a technique of ensuring a function cannot be called in succession too rapidly. We are making a saveClaps function that when called will wait for 700 milliseconds to see if it's called again. If it is called again within 700ms then it starts the time over and waits again. It does this until 700ms has passed with no additional calls. Then the callback is called just once. The end result for us means that if someone claps many times quickly, we will wait for a pause in their clapping before we do the network request.

From a state standpoint, I need to keep track of how many claps the user has been accumulating for the eventual debounced network request. Then I need to also keep track of how many claps there are in total. This is why each time the button is clicked the queuedClaps gets incremented immediately and then we call saveClaps() which eventually moves queuedClaps to claps and resets the queue.

So what's the Problem?

Now that we're writing hooks in React, we need to change our mindset a little. Remember that our BlogClaps() component is a function will be called over and over (for state changes among other things). Each time it's called we're re-assigning saveClaps with a new debounced function. To fix this, I tried using a ref since they are like "instance variables" that don't change over time:

const saveClaps = useRef(debounce(claps => {  setClaps(claps)
  setQueueClaps(0)
}, 700))

function clap() {
  const newClaps = queueClaps + 1
  setQueueClaps(newClaps)
  saveClaps.current(newClaps)}

Just remember that whatever you pass into useRef is available as the .current property of the ref returned. So in this case, saveClaps.current() is the debounced function.

See a working version of what we have so far on Stackblitz

Converting to useRef works, almost

It technically works, although we're calling debounce() with each time our component re-renders. The current value of a ref doesn't change so the multiple calls to debounce() are not too harmful, but it's still a little annoying to have this going on.

Taking a step back, do we need lodash?

After some consideration and trying to fix the multiple calls to lodash (and talking to Ryan), we thought about abandoning it for a simple timeout solution instead.

What if BlogClaps looked like this:

function BlogClaps() {
  const [claps, setClaps] = useState(0)
  const [queueClaps, setQueueClaps] = useState(0)
  const timeoutRef = useRef(null)

  function clap() {
    clearTimeout(timeoutRef.current)
    const newClaps = queueClaps + 1
    setQueueClaps(newClaps)
    timeoutRef.current = setTimeout(() => {
      // This is where the network request would be, when it
      // resolves we would set `claps` to be whatever the network
      // says it is, and then reset `queueClaps`. But for now
      // we're skipping that part and just setting claps manually:
      setClaps(newClaps)
      setQueueClaps(0)
    }, 700)
  }

  return (
    <div>
      <button onClick={clap}>
        Clap
      </button>{' '}
      {queueClaps}/{claps}
    </div>
  )
}

This works too. Now we're just incrementing queueClaps for each click and then after 700ms we'll sync to the server. To prevent overlapping timeouts, we'll just clear the current timeout each time clap() is called.

Although, persisting the claps to the server is really a side-effect of clapping and it could also be decoupled from the clap function. When we clap, we just want to save our claps in state and then let a side-effect function deal with the synchronization.

Let's try useEffect to manage our setTimeout:

function BlogClaps() {
  const [claps, setClaps] = useState(0)
  const [queueClaps, setQueueClaps] = useState(0)

  useEffect(() => {
    if (queueClaps > 0) {
      const timeout = setTimeout(() => {
        setClaps(queueClaps)
        setQueueClaps(0)
      }, 700)
      return () => clearTimeout(timeout)
    }
  }, [queueClaps])

  function clap() {
    setQueueClaps(queueClaps + 1)
  }

  return (
    <div>
      <button onClick={clap}>
        Clap
      </button>{' '}
      {queueClaps}/{claps}
    </div>
  )
}

See a working version at Stackblitz

Final with Network Request

Next we'll do the real network requests with our helper functions we wrote for Firebase. Notice that we're also passing a URL slug for the blog post so we know where to save to:

function BlogClaps({ slug }) {
  const [loading, setLoading] = useState(true)
  const [claps, setClaps] = useState(0)
  const [queueClaps, setQueueClaps] = useState(0)

  useEffect(() => {
    // When the component loads, get the current clap count
    getBlogPostClaps(slug).then(claps => {
      setLoading(false)
      // Set the clap count
      setClaps(claps)
    })
  }, [slug])

  // This effect gets called anytime the queueClaps gets changed
  useEffect(() => {
    if (queueClaps > 0) {
      // Create a timeout if queueClaps was bigger than 0
      const timeout = setTimeout(() => {
        // After 700ms, send the network request and then
        // update the claps based on the network, reset
        // queueClaps
        addBlogPostClaps(slug, queueClaps).then(claps => {
          setClaps(claps)
          setQueueClaps(0)
        })
      }, 700)
      return () => clearTimeout(timeout)
    }
  }, [queueClaps])

  function clap() {
    setQueueClaps(queueClaps + 1)
  }

  // Notice the changes to the JSX too:
  return (
    <div>
      <button onClick={clap}>
        {loading
          ? 'Loading...'
          : `${(claps + queueClaps).toLocaleString()} claps`}
      </button>
    </div>
  )
}

See the final version on StackBlitz

Now it's up to you to design it!

Did someone say Firebase?

We're loving firebase for ReactTraining.com, it's been fantastic for data storage, hosting, cloud functions, deployments, and cost. If you're interested in learning about all that Firebase has to offer, we have an online course available.

Loading...

While we don't have blog comments, we tweeted about this posting when it went live so we welcome your comments there:


React Router

Michael Jackson and Ryan Florence create the React libraries that you use in your apps like React Router and Reach UI. All of our trainers are experts in React and JavaScript so let us share our knowledge with you and your team!

I Love React
© React Training 2019