Using request animation frame in React

Viewed 2839

I'm reading this article and I'm not sure I understand how the final hook works.

Here is the code:

const useAnimationFrame = (callback) => {
  const requestRef = useRef();
  const previousTimeRef = useRef();

  const animate = (time) => {
    if (previousTimeRef.current !== undefined) {
      const deltaTime = time - previousTimeRef.current;
      callback(deltaTime);
    }
    previousTimeRef.current = time;
    requestRef.current = requestAnimationFrame(animate);
  };

  useEffect(() => {
    requestRef.current = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(requestRef.current);
  }, []);
}

and used for example in this way:

const [count, setCount] = useState(0);

useAnimationFrame((deltaTime) => {
  setCount((prevCount) => {
    return prevCount + 1;
  });
});

Ok, the goal is have a number value that is incremented every frame.

I can explain what is happens running this code:

  1. the component create a local state with useState(0)

  2. then the useAnimationFrame hook is called using this callback as parameter:

    (deltaTime) => {
      setCount((prevCount) => {
        return prevCount + 1;
      });
    }
    

the function takes a number as input and increment ste state value of one each time it is called.

  1. useAnimationFrame is a function that takes another function as a parameter (a callback). It creates two refs. At the first time it is executed (because of the []) it calls the useEffect. It saves in requestRef.current the timestamp the requestAnimationFrame returns. The requestRef.current calls the animate function that computes the delta time between the request animation frames (the previous and the current) and then call the callback with this value so it calls the setCount. Then it updates the current refs values and recall the requestAnimationFrame.

So the cycle should be:

component 
  > count = 0
useAnimationFrame             <--------------+
  > requestRef = ?                           |
  > previousTimeRef = ?                      |
    useEffect                                |
      animate                                |
        > deltaTime = delta#1                |
        > count = 1                          |
        > previousTimeRef.current = time#1   |
        > requestRef.current = time#2 -------+
      > requestRef.current = timestamp#1

Am I wrong?

1 Answers

It might be helpful to track the function signatures of requestAnimationFrame and cancelAnimationFrame.

requestAnimationFrame takes a single argument, a callback function. The callback function itself receives a single timestamp argument (DOMHighResTimeStamp)

cancelAnimationFrame takes a single argument, the id of the requestAnimationFrame which you want to cancel.

So time in the animate callback function is the single argument received via the api, a DOMHighResTimeStamp similar to the one returned by performance.now(), indicating the point in time when requestAnimationFrame() starts to execute callback functions.

 const animate = (time) => {

This is a check to see if the hook has already run 1x. If it has, update the parent React scope with the new time minus the previous time

    if (previousTimeRef.current !== undefined) {
      const deltaTime = time - previousTimeRef.current;
      callback(deltaTime);
    }

Once the hook is confirmed as having run, save the DOMHighResTimeStamp for future calculations

    previousTimeRef.current = time;

After this, it gets a bit interesting and I am not sure this is best approach. It may even be a bug. The code sets a new listener and updates the ref with the latest id, based on the result of a new invocation.

Just from reading the code, I am not sure the original listener ever gets cancelled. I suspect it is not.

    /// this is an id
    requestRef.current = requestAnimationFrame(animate);

I don't have access to a running version, but I would suggest removing the requestRef.current altogether and see if the clean-up happens as expected when the useEffect clean-up happens e.g.

  useEffect(() => {
    const id = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(id);
  }, []);

This will also simplify the embedded refs as well to make reading a bit more clear.

Related