Performant animations with requestAnimationFrame() and React hooks

Artiom Mozgovoy
4 min readSep 30, 2021
Photo of Apartments house in Moscow by me

For some reason, we sometimes still trying to use really old techniques and hacks to achieve smooth-looking animations in browsers. But as for the latest ~6 years, in cases that cannot be covered by CSS animations, you should go with requestAnimationFrame browser API as it is well supported by all major browsers and it is THE TOOL for script-based animations handling. Just let setInterval and setTimeout be forgotten when you thinking about animations 🙏

Animation frame API contains 2 methods requestAnimationFrame and cancelAnimationFrame. They allow running a callback function when the browser will be preparing a repaint. Usually, that’s happening frequently, but the exact time depends on the browser. Also when a page is in the background, there are no repaints at all, so the callback won’t run: the animation will be suspended and won’t consume resources. That’s great!

If you are in hurry just scroll down and grab your complete implementation or install an NPM package written with TypeScript, for all other readers — let’s try to implement it together.

Usage with React

The problem that I faced with the usage of requestAnimationFrame inside of React apps is that it’s requiring a bit of additional hacking to work properly:

  • in some cases, we want to stop/start animation based on component state/props/values returned from hooks
  • whole logic should be reusable and defined in one place

With these requirements in mind, the most appropriate place for isolating interactions with requestAnimationFrame is inside of a hook, which can be used by any component.

Naïve hook implementation

Most naïve implementation for continuous “infinite” animation would still include some small tricks:

First thing is that for keeping the current animation frame ID we should use useRef as it is not bound to react life cycle and frequently changed value will not trigger any side effects/rerenders inside of react.

This is a really handy thing about refs in react to keep in mind, especially when you want to store and update some values “outside” of the component life cycle.

The second is simpler — in useEffect unmount handler we want to stop our animation cycle ASAP when a component is not present on screen anymore.

Add start and stop animation control

Now let’s add to our naive implementation some additional parameters to provide hook users with the ability to turn animation on and off. For this, we would go with kinda the same approach as build-in react hooks but provide only property to useAnimationFrame hook, instead of dependencies array:

This one is pretty simple actually and just requires us to add shouldAnimate parameter to hook and use it as a dependency in useEffect .

Use-cases for that kind of behavior can be situations when you shouldn't start animation before some action would be triggered by the user and stop it when the user is hovering over the animated element.

Add duration and progress indicator

In most cases, you know in advance for how long you want to run animation and know what state the user should see at the end of it.

In this case, you want to pass some value for the duration of the animation to hook and process somehow our current position in that timeline in the animation handler. For example, you want to animate scroll and do it for 5seconds, in that case, you can calculate where scroll should be at 1st second — if animation is linear it should be at 1/5 if whole animation range, at 2nd — 2/5, and so on. So we need the current progress percentage:

This part is a bit trickier, but the general concept of it is still simple:

  • pass duration to hook
  • in every frame calculate current progress (timeFraction) in the timeline
  • pass current progress to nextAnimationFrameHandler to later be used to calculate HTML element position/parameters

At this point, our hook is already good to go, so you can grab it and experiment and play with a little demo in this codepen sandbox.

And that's it. Now our hook can be used by just providing an animation frame handler as we defined default values for most common cases.

🚨 Last but really important note — keep your animation frame handler as simple and performant as possible with the least amount of dependencies and transformations. It will be called frequently and this can slow down the whole website.

May the smooth animation be with you 💅

UPDATE: I decided to publish an NPM package written with TypeScript, so now you can use it instead of copying that code to your project 🥳 🎉

https://www.npmjs.com/package/use-request-animation-frame

--

--