Search code examples
reactjsreact-hooksrequestanimationframe

Redux state not updating in requestAnimationFrame


When updating Redux state, the updated state is received in the component, but not within the requestAnimationFrame function.

i.e. we can log out the updated state (backgroundMode) near the top of the component declaration - updates are logged as expected:

const VideoCanvas = ({ videoRef }: IVideoCanvasProps) => {
  ...
  console.log("backgroundMode", backgroundMode);
  ...

But in the requestAnimationFrame function, only the initial value (None) is logged out for each frame.

  async function animate() {
    ...

    // Does not receive updates from Redux store
    console.log("animate backgroundMode", backgroundMode);
    
    ...

    animationFrame.current = requestAnimationFrame(animate);

The console output looks something like the following:

backgroundMode None
(20) VideoCanvas.tsx:71 animate backgroundMode None

// change dropdown to blur mode
VideoCanvas.tsx:26 backgroundMode Blur
(23) VideoCanvas.tsx:71 animate backgroundMode None

// change dropdown to mask mode
VideoCanvas.tsx:26 backgroundMode Mask
(62) VideoCanvas.tsx:71 animate backgroundMode None

Adding backgroundMode to the dependencies of the useEffect initiating the animation, only causes two animation threads to be spawned, worsening the problem. As it stands, this effect looks like:

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

The file in question can be viewed in full on my GitHub here

Any suggestions would be much appreciated!


Solution

  • This sort of sounds like the old "stale state enclosure" issue happening in the animate function. Since you are recursively calling requestAnimationFrame and passing the value of animate from the render cycle the animations are started in, you may want to cache a copy of backgroundMode also in a ref so that it can be updated anytime, and also the current value can be read anytime. Use an useEffect hook to update the cache.

    const backgroundModeRef = React.useRef(backgroundMode);
    
    React.useEffect(() => {
      backgroundModeRef.current = backgroundMode;
    }, [backgroundMode]);
    

    In the animate function, refer to the cached ref value.

    async function animate() {
      ...
    
      // Does not receive updates from Redux store
      console.log("animate backgroundMode", backgroundModeRef.current);
    
      ...
    
      animationFrame.current = requestAnimationFrame(animate);