Using requestAnimationFrame in React for Smoothest Animations
This article will discuss the
requestAnimationFramemethod, 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
useStatehook with thevalueandsetValuevariables. - We also utilized the
useRefhook to declare three other variables. This ensures they remain unaffected and don’t trigger any side effects when updated. - We initialized the
runAgainandprogressTimerrefvariables with theperformance.now()method. This initialization captures the currenttimestampwhen the page first loaded. It also synchronizes their values with thetimestampfrom therequestAnimationFramecallbackargument. - The
requestIdRefvariable stores the value therequestAnimationFramemethod returns. - We defined the
nextFramefunction as thecallbackfor therequestAnimationFramemethod. This function wraps the counter logic, and therequestAnimationFramerecursively invokes it. - We ensured the conditional code block for
runAgain.currentandprogressTimer.currentexecutes. This event occurs when they are less than or equal to the currenttimestampvalue. - We updated
runAgain.currentandprogressTimer.currentbelow the conditional statement code blocks. These updates ensure a smooth repaint after a designated interval. - Inside a
useEffectcallback, we invoked therequestAnimationFramewith thenextFramefunction. We also provided an empty dependency array to theuseEffectto ensure it runs once. - Finally, we cleaned up after the initial invocation. In the returned function, ensure to pass
requestIdRef.currentto thecancelAnimationFramemethod.
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.currentvalue dynamic. - The
progressTimer.currentgets updated with the currenttimestampand 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.