js
Link to this headingContext
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:
Link to this headingUsage
This example uses the hook to create a "laggy" clock (a clock that only updates once every few seconds):
jsx
Link to this headingExplanation
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 tominDelay
and/ormaxDelay
- 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.