React: Warning: ... To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

Created on 14 Nov 2018  ·  3Comments  ·  Source: facebook/react

I get this:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in Camera (created by App)

The useEffect doesn't have subscriptions in itself.
But, it calls startRaf() from useRaf which creates a subscription. However, it comes with a cleanup function!

To reproduce:

  1. Visit https://codesandbox.io/s/81m4jm616j
  2. Click the "Open In New Window" icon (next to the address bar). This will allow you to enable the camera.
  3. Open the console.
  4. Click the "Stop" button.

You should see the warning above in the console.

What am I doing wrong in my hooks?

Most helpful comment

Well, I found the problem of my code. It's because I mistakenly use useEffect, and I should use useLayoutEffect instead when I want to use requestAnimationFrame API.

In react docs, there's a tip about this, and I think your code @jacobp100 may face the same problem:

Unlike componentDidMount or componentDidUpdate, effects scheduled with useEffect don’t block the browser from updating the screen. This makes your app feel more responsive. The majority of effects don’t need to happen synchronously. In the uncommon cases where they do (such as measuring the layout), there is a separate useLayoutEffect Hook with an API identical to useEffect.

:-)

All 3 comments

Every time you call useRaf you replace your unmount handler with one that references a different version of rafID.

Try instead with refs,

export default function useRaf() {
  let [counter, setCounter] = useState(0);
  let rafID = useRef(null);

  function loop() {
    setCounter(counter => counter + 1);
    rafID.current = requestAnimationFrame(loop);
  }

  function start() {
    rafID.current = requestAnimationFrame(loop);
  }

  useEffect(() => {
    return () => {
      cancelAnimationFrame(rafID.current);
    };
  }, []);

  return [counter, start];
}

@jacobp100 I think this solution may not work perfectly, though. As cancelAnimationFrame method is executed in the clean-up period, the loop method continues so a new rafID.current will be assigned, even though you already canvel previous rafID.current.

And I don't think we need to use useRef here, a normal variable can already handle the storage of requestAnimationFrame ID? Change let rafID = useRef(null); to let rafID = null;.

A safe way to guarantee current animation's stop is to add conditions in your main loop method, loop method can be rewrited to:

function loop() {
  setCounter(counter => counter + 1);
  rafID.current = YOUR_CANCEL_CONDITION ? 
    cancelAnimationFrame(rafID.current) 
    : 
    ArequestAnimationFrame(loop);
}

in which YOUR_CANCEL_CONDITION is the logic you should handle with.

Well, I found the problem of my code. It's because I mistakenly use useEffect, and I should use useLayoutEffect instead when I want to use requestAnimationFrame API.

In react docs, there's a tip about this, and I think your code @jacobp100 may face the same problem:

Unlike componentDidMount or componentDidUpdate, effects scheduled with useEffect don’t block the browser from updating the screen. This makes your app feel more responsive. The majority of effects don’t need to happen synchronously. In the uncommon cases where they do (such as measuring the layout), there is a separate useLayoutEffect Hook with an API identical to useEffect.

:-)

Was this page helpful?
0 / 5 - 0 ratings