Back

Using requestAnimationFrame in React for Smoothest Animations

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 in React, and showing how to use it to create smooth progress loaders.

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 the value and setValue 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 and progressTimer ref variables with the performance.now() method. This initialization captures the current timestamp when the page first loaded. It also synchronizes their values with the timestamp from the requestAnimationFrame callback argument.
  • The requestIdRef variable stores the value the requestAnimationFrame method returns.
  • We defined the nextFrame function as the callback for the requestAnimationFrame method. This function wraps the counter logic, and the requestAnimationFrame recursively invokes it.
  • We ensured the conditional code block for runAgain.current and progressTimer.current executes. This event occurs when they are less than or equal to the current timestamp value.
  • We updated runAgain.current and progressTimer.current below the conditional statement code blocks. These updates ensure a smooth repaint after a designated interval.
  • Inside a useEffect callback, we invoked the requestAnimationFrame with the nextFrame function. We also provided an empty dependency array to the useEffect to ensure it runs once.
  • Finally, we cleaned up after the initial invocation. In the returned function, ensure to pass requestIdRef.current to the cancelAnimationFrame 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 current timestamp 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: A progress bar 50% filled 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: A 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: A progress indicator moving in a circular 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.

OpenReplay