Using requestAnimationFrame in React for Smoothest Animations
This article will discuss the
requestAnimationFrame
method, delving into why you should use it and ways to leverage it in performing animation inReact
, and showing how to use it to create smooth progress loaders.
Discover how at OpenReplay.com.
Animation brings life to a digital application. Modern web applications leverage animations and transitions to create an engaging user experience. Proper application of animations can build a strong connection between the users and the content of a web application. In the past, developers relied on setTimeout
and setInterval
to perform JavaScript animations. These traditional methods invoke a function 60 times per second to imitate a smooth animation. Their inconsistencies and unnecessary repaint prompted a more performant and reliable method. Paul Irish introduced the requestAnimationFrame
(rAF) in 2011 as a better approach for web animation. This method allows the browser to decide the repaint rate, optimizing web animations.
What is requestAnimationFrame
?
The requestAnimationFrame
is a method provided by browsers for animations in JavaScript. It requests the browser to invoke a specified function and update an animation. The requestAnimationFrame
receives a single argument, a callback
function, called at intervals. The frequency of callback
function calls matches a display refresh rate of 60Hz. This frequency is equal to 60 cycles/frames per second. Its syntax is as follows:
requestAnimationFrame(callback)
Invoking the requestAnimationFrame
schedules a single call to the callback
function. Animating another frame involves repeatedly calling the requestAnimationFrame
method. Nevertheless, understanding its role in efficient animation is essential.
What is the Role of requestAnimationFrame
in Efficient Animation?
The requestAnimationFrame
method plays a vital role in efficient animation. It synchronizes animations with the browser’s rendering cycle. The higher the frame rate, the smoother the animation. But, a higher frame rate requires more resources, impacting performance.
Unlike the conventional approach, the requestAnimationFrame
can achieve optimal frame rate by:
- Enabling the browser to decide the next repaint cycle and optimize it.
- Limiting animations to a 60Hz display refresh rate, ensuring smooth and efficient animations.
- Reducing unnecessary central processing unit (CPU) usage and conserves resources.
The following section will discuss stopping an animation request.
Canceling the requestionAnimationFrame
JavaScript animation with requestAnimationFrame
involves recursively calling a function to execute a particular code block. Allowing further calls to the callback
by requestAnimationFrame
after completing an animation can lead to memory leaks and affect performance. Fortunately, JavaScript provides the cancelAnimationFrame
method to stop further requests to the browser to refresh the callback
function after a completed animation. Consider the syntax below:
function callback() {
// animation code block
}
const requestId = requestAnimationFrame(callback);
cancelAnimationFrame(requestId);
When invoked, requestAnimationFrame
returns a long integer value uniquely identifying the entry in the callback
list. This value gets assigned to the requestId
variable. Passing the requestId
to the cancelAnimationFrame
method cancels the refresh callback
request.
Why requestAnimationFrame
?
The requestAnimationFrame
provides an efficient way to schedule animation in web browsers. The requestAnimationFrame
offers several benefits, including:
- It allows the browser to update animation states precisely between frames. This consistency results in smooth, higher-fidelity animations.
- It repaints less often in hidden or inactive tabs and during CPU overload. This phenomenon helps conserve system resources, leading to much longer battery life.
- It leverages hardware acceleration in modern browsers to optimize animations. This approach enhances performance and reduces CPU load in devices with limited resources.
Utilizing requestAnimationFrame
in React
Performing animations with requestAnimationFrame
in React
introduces intricacies distinct from Vanilla JavaScript. It involves a good understanding of state
, props
, and React's
life-cycle methods. Efficient side effects management is essential for consistent performance and behavior. Let’s see how to achieve these in the later sub-sections.
Managing Side-effects with Hooks
React
provides several hooks to manage side effects within a React
component. For this implementation, we will use the essential hooks. Consider the following approach to create a progress counter that stops at 100:
import { useEffect, useRef, useState } from "react";
export default function App() {
const [value, setValue] = useState(0);
const runAgain = useRef(performance.now() + 100);
const progressTimer = useRef(performance.now() + 100);
const requestIdRef = useRef(0);
function nextFrame(timestamp) {
if (runAgain.current <= timestamp) {
// Run the animation
if (progressTimer.current <= timestamp) {
// When the native timestamp catches up to the set interval
//animate the element with the current value
const nextValue = 1;
setValue((preValue) =>
preValue + nextValue < 100 ? (preValue += nextValue) : 100,
);
progressTimer.current =
timestamp + 100;
}
runAgain.current = timestamp + 100;
}
// batch the animation for the next frame paint
requestIdRef.current = requestAnimationFrame(nextFrame);
}
useEffect(function () {
requestIdRef.current = requestAnimationFrame(nextFrame);
return () => cancelAnimationFrame(requestIdRef.current);
}, []);
return (
<div className="App">
<p>{value}</p>
</div>
);
}
Let’s explain what is happening here:
- We added a way to keep track of the counter value using the
useState
hook with thevalue
andsetValue
variables. - We also utilized the
useRef
hook to declare three other variables. This ensures they remain unaffected and don’t trigger any side effects when updated. - We initialized the
runAgain
andprogressTimer
ref
variables with theperformance.now()
method. This initialization captures the currenttimestamp
when the page first loaded. It also synchronizes their values with thetimestamp
from therequestAnimationFrame
callback
argument. - The
requestIdRef
variable stores the value therequestAnimationFrame
method returns. - We defined the
nextFrame
function as thecallback
for therequestAnimationFrame
method. This function wraps the counter logic, and therequestAnimationFrame
recursively invokes it. - We ensured the conditional code block for
runAgain.current
andprogressTimer.current
executes. This event occurs when they are less than or equal to the currenttimestamp
value. - We updated
runAgain.current
andprogressTimer.current
below the conditional statement code blocks. These updates ensure a smooth repaint after a designated interval. - Inside a
useEffect
callback
, we invoked therequestAnimationFrame
with thenextFrame
function. We also provided an empty dependency array to theuseEffect
to ensure it runs once. - Finally, we cleaned up after the initial invocation. In the returned function, ensure to pass
requestIdRef.current
to thecancelAnimationFrame
method.
But there is a slight bug with this code. Currently, the timed loop doesn’t stop once it starts. While this behavior is acceptable in specific scenarios, it defeats our goal. To address this, consider the following:
useEffect(
function () {
if (value >= 100) {
cancelAnimationFrame(requestIdRef.current);
}
},
[value],
);
The code snippet above shows a useEffect
keeping track of the progress value. Placing this code below the previous useEffect
cancels the animation request once completed.
Extracting the requestAnimationFrame
Logic into a Custom Hook
The preceding section shows a progress counter implementation with the requestAnimationFrame
method. Unfortunately, this approach becomes tedious when duplicating the same logic across several components. So, moving the whole logic to a custom hook becomes necessary and helpful. Let us create a reusable hook to encapsulate our progress counter logic:
// useProgressLoader.jsx
function useProgressLoader(interval = 2000) {
// Values to update
const [value, setValue] = useState(0);
const runAgain = useRef(performance.now() + 100);
const progressTimer = useRef(performance.now() + 100);
const requestIdRef = useRef(0);
function nextFrame(timestamp) {
if (runAgain.current <= timestamp) {
// Run the animation
if (progressTimer.current <= timestamp) {
// When the native timestamp catches up to the set interval
// animate the element with the current value
const nextValue = Math.floor(Math.random() * 25) + 1;
setValue((prevValue) =>
(prevValue + nextValue < 100 ? (prevValue += nextValue) : 100)
);
// Change to a random interval
progressTimer.current =
timestamp + Math.floor(Math.random() * (interval - 800)) + 500;
}
runAgain.current = timestamp + 100;
}
// batch the animation for the next frame paint
requestIdRef.current = requestAnimationFrame(nextFrame);
}
useEffect(function () {
requestIdRef.current = requestAnimationFrame(nextFrame);
return () => cancelAnimationFrame(requestIdRef.current);
}, []);
useEffect(() => {
if (value >= 100) {
// Stop the animation
cancelAnimationFrame(requestIdRef.current);
}
}, [value]);
return { value };
}
The progress logic is almost identical to the previous implementation, with minor changes:
- We made the interval used to update the
progressTimer.current
value dynamic. - The
progressTimer.current
gets updated with the currenttimestamp
and a random interval. - We ensured the progress increased with random values from 1 to 25.
Based on these improvements, let’s build an animated progress loader.
Creating Progress Loader Animations
So far, we demonstrated a basic progress counter with our requestAnimationFrame
setup. Now, let’s develop more advanced loader animations, such as:
- A progress bar
- A circular progress indicator
Implementing a Progress Bar
A circular progress indicator provides visual feedback about an ongoing task. It moves in a circular motion toward its starting position. Let’s show this with the code example below:
// ProgressBar.jsx
import "./progressbar.css";
const ProgressBar = () => {
const value = 50;
return (
<section className="">
<div
id="progress"
role="progressbar"
aria-valuenow={value}
aria-valuemax={100}
>
<div
style={{ width: value + "%" }}
id="indicator"
aria-label="Progress indicator"
></div>
<p>{value}%</p>
</div>
</section>
);
};
export default ProgressBar;
In the code above, we have created a component to encapsulate the progress bar UI logic. We ensured accessibility by setting the progressbar
value on the div
tag role
attribute. We also provided the values for the aria-valuenow
and aria-valuemax
attributes. Let’s define the CSS properties for a visually appealing progress bar:
/* progressbar.css */
#progress {
height: 2rem;
width: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
#progress > p {
font-size: 12px;
position: relative;
z-index: 2;
}
#indicator {
top: 0;
position: absolute;
left: 0;
height: 100%;
background: #3a9a3a;
transition: width 0.4s ease-out;
-moz-transition: width 0.4s ease-out;
-webkit-transition: width 0.4s ease-out;
}
In the CSS styles above, we set the width and height of the #progress
selector. Ensuring the bar is wide enough for the indicator. The CSS flexbox
values center the label in vertical and horizontal space. To display the #indicator
selector, we defined its position as absolute. Furthermore, we set the top and left properties to zero and the height to 100%. These values enable the indicator to fit into the bar’s dimension. Finally, defining the transition property facilitates a smooth repaint.
The following code renders the progress bar in the app:
// App.jsx
import ProgressBar from "./ProgressBar";
export default function App() {
return (
<div className="App">
<ProgressBar />
</div>
);
}
For demonstration purposes, we set an initial value of 50. Here is a snapshot of the rendered progress bar: To animate the bar, let’s replace the line that initializes the progress value.
// ProgressBar.jsx
import useProgressLoader from "./useProgressLoader";
const ProgressBar = () => {
const { value } = useProgressLoader();
return (
{ /* Progress bar UI JSX */ }
);
};
In the code above, we used the value returned by the useProgressLoader
hook instead. This value ensures the rendered horizontal displays a visual progress.
Below is a visual result of the horizontal progress bar in motion:
Creating a Circular Progress Indicator
A circular progress indicator is a UI element that provides visual feedback to convey the idea of an ongoing task or process by moving in a circular motion toward its starting position. Let’s demonstrate this with the code example below:
// CircularProgressLoader.jsx
import "./circular-progress-loader.css";
import useProgressLoader from "./useProgressLoader";
const CircularProgressLoader = () => {
const { value } = useProgressLoader();
return (
<section className="circle-wrapper">
<div
className="circular-progress"
role="progressbar"
aria-valuemin={0}
aria-valuenow={value}
aria-valuemax={100}
style={{
background: `conic-gradient(#F4A443 ${(360 / 100) * value}deg, #F7F8F7 0deg)`,
}}
>
<p aria-label="Generate progress value" className="progress-label">
{value}%
</p>
</div>
</section>
);
};
export default CircularProgressLoader;
In the code snippet above, we employed three accessible elements. The innermost element displays the current fill percent. Furthermore, we applied the CSS conic-gradient background value to create a color-filled area. Finally, incorporating the value from the useProgressLoader
hook, animated the loader. Let’s define the CSS properties for a circular area:
/* circular-progress-loader.css */
.circular-progress {
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 300px;
width: 300px;
border-radius: 50%;
}
.circular-progress .progress-label {
position: absolute;
z-index: 2;
}
To create a circular shape, we set equal width and height and border-radius
of 50%. Defining the flex-box
values centers the label in the horizontal and vertical space.
The following code displays the progress indicator:
// App.jsx
import CircularProgressLoader from "./CircularProgressLoader";
export default function App() {
return (
<div className="App">
<CircularProgressLoader />
</div>
);
}
Below is a visual illustration of the circular progress indicator in motion:
Final Thoughts
This article explored how the requestAnimationFrame
method surpasses conventional means. We also discussed its role and benefits in web animation. Furthermore, we ensured modularity and reusability of animation logic with a custom hook.
As a powerful tool, the requestAnimationFrame
method offers several benefits and use cases. Understanding its proper usage is crucial in optimizing animations and enhancing user experience.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.