Create a React hook that can simulate a pulse-like cycle between animation states through different durations per animation state.
Here's the hook in an example.
We have an animation heartbeat player that switches between four animation states, each with its own user-configurable duration.
We have a ping service that sends out a ping to our component on an interval, or heartbeat, and each ping kicks off an animation cycle. This cycle goes through the sequence:
The original implementation was built into a class component using a local MobX observable and nested
setTimeouts and has worked very well for the past few years.
This is a rough idea of how the nested
setTimeouts are set up inside of the ping.
We're at a point where we need to update the renderer housing this logic and I thought I would attempt to do it with a functional component.
The four animation states we need to switch between are
rest; while each of our widgets has CSS animations that are tied to a
Each of these animation states needs its own duration that is user-configurable.
First, I tried to implement something similar to what we see above in a
setState. The downside here is that the
useEffect is new every render so I wasn't able to track timeouts effectively.
The second thing I tried, was to leverage the
useInterval hook that Dan Abramov created. The downside here is that the callback is a
ref so it never changes, which means I can't pass it a different callback for each step/duration.
Finally, I settled on a mix of
refs for persistence and a custom hook to handle the
I thought I would be able to jam the
setTimeouts in an array and use a
for of loop to run them, one by one. This ended up running them "out of order."
I ended up coming across two solutions that helped me piece it together, How to resolve a useReducer's dispatch function inside a promise in ReactJS and Why Using reduce() to Sequentially Resolve Promises Works.
The idea here is that each is wrapped in a
Promise and added to an array where we can loop over them in a
reduce, awaiting the previous
Promise before starting the next.
This worked like a charm!
The custom hook is where the magic lies. We start with two
useStates, one for the animation state and another to determine if the animation cycle is running. We'll return the
status and the ability to set
isRunning so our component can turn it on/off.
Next, we set up a
useEffect that will watch
isRunning to see if the cycle should start. In this hook, we'll have two functions, one that sets up the
Promises and another that will run the
reduce over the
In our component, we can now run our hook and have a
ref that catches the ping from our player service, which sets
true, starting the animation cycle.
Now we have an animation cycler that can be started from our component, and the best part is, we can have our component be functional 😀.
I definitely learned more about
refs and how to work with promises during this feature. Hopefully, there will some more refactors to a functional component that can challenge other areas.