Snippet

useRandomInterval

js

In JavaScript, we have two primitives for scheduling something in the future based on an amount of time:

  • setTimeout
  • setInterval

setTimeout is great for one-off events, and setInterval is great for things that happen on a fixed schedule… but what if we want something to happen a bit more spontaneously?

For example, consider this . The goal is to schedule new sparkles to be produced in an ongoing fashion, but not uniformly; Each sparkle appears between 20ms and 500ms after the last one. This variation makes it feel more organic / less robotic.

This hook is great for animations and microinteractions. If you're generating particles for a confetti or firework effect, having a random delay between each particle can add a lot of life to the effect.

Here the hook is used to change the "heartbeat" of a pulsing circle. The slider controls the min and max time values. Notice how the effect changes depending on their position:

This example uses the hook to create a "laggy" clock (a clock that only updates once every few seconds):

jsx

This hook is not simple, and it's because we have to be pretty crafty about how we make sure a relevant callback is made available to the hook; this problem and solution is explored in depth in Dan Abramov's blog post on setInterval. If you haven't already read it, I would recommend starting there.

In order to create a "random" interval, we need to use setTimeout. On every "tick", we schedule the next iteration a random amount of time in the future, based on the min and max values provided.

At its core, here's what this trick looks like:

js

A function has some sort of effect (doSomething), but it also calls itself recursively, after a random amount of time. This continues indefinitely, with each loop being between 0 and 5 seconds after the previous one.

There are two ways to "cancel" this random interval:

  • Pass a null value to minDelay and/or maxDelay
  • Call the returned cancel function

The first method is the preferred one; by setting a null delay length, the loop will stop getting called. This is because our effect has some cleanup; whenever the delays change, it interrupts the current timeout:

js

If minDelay or maxDelay is null, the cleanup will run to clear the timeout, but no new timeout will be set.

Finally, there is the cancel function:

js

This provides an imperative way to interrupt the loop without triggering a re-render. It is an escape hatch and shouldn't be used in most cases.

It's wrapped in useCallback so that it can safely be passed to child elements without busting a React.memo component. While I might consider this a premature optimization in a typical case, I think it's a fair optimization for generalized, reusable components like this one.