简体   繁体   中英

UseEffect hook error, Can't perform a React state update on an unmounted component

I am trying to set state in my useEffect hook when the appState is active, however, I get the warning. How can I set state when my app is active?

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.

useEffect(() => {
    const subscription = AppState.addEventListener(
      "change",
      async (nextAppState) => {
        if (
          appState.current.match(/inactive|background/) &&
          nextAppState === "active"
        ) {
          setAppStateVisible(true); // set state to true
        }

        appState.current = nextAppState;
        setAppStateVisible(appState.current);
      }
    );

    return () => {
      subscription.remove();
    };
  }, []);

Yeah, this is a common problem in React that has no official solution as of yet (AFAIK).

You can do something like this (it works, but half of the Inte.net see this as anti-pattern):

useEffect(() => {
   let isMounted = true;
   const subscription = AppState.addEventListener("change", 
      async (nextAppState) => {
    if (appState.current.match(/inactive|background/) && nextAppState === "active" && isMounted) { 
        setAppStateVisible(true); // set state to true
    }

    appState.current = nextAppState;

    if(isMounted) {
        setAppStateVisible(appState.current);
    }

    });

  return () => {
    isMounted = false;
    subscription.remove();
  };
}, []);

Basically the idea is to wrap your setAppStateVisible calls in a protective check whether the component is mounted or not - because you should not update the state of the component if it is unmounted (as the message says).

Since you are removing (cancelling) your subscription in a cleanup callback, I'm guessing you probably have one or more await s in your async function (otherwise, why make it an async function?) though you haven't shown any. Since the await s mean that your code pauses then continues again later (when the promise it's await ing is settled), you need to allow for the possibility your component has been unmounted in the meantime.

Ideally, subscription would have a way for you to check if it's been removed (like AbortSignal 's aborted flag). I can't find any documentation for it, though;AppState.addEventListener says it's an EventSubscription , but doesn't provide a link, and I can't find any docs for EventSubscription .

If it has one, you want to use it. Otherwise, you can add a simple flag in addition to your code removing the subscription:

useEffect(() => {
    let cancelled = false;                              // ***
    const subscription = AppState.addEventListener(
        "change",
        async (nextAppState) => {
            if (cancelled) {                            //
                return;                                 // ***
            }                                           //
            if (
                appState.current.match(/inactive|background/) &&
                nextAppState === "active"
            ) {
                setAppStateVisible(true); // set state to true
            }

            await something();                          // Example of an `await`
            if (cancelled) {                            //
                return;                                 // ***
            }                                           //
    
            appState.current = nextAppState;
            setAppStateVisible(appState.current);
        }
    );

    return () => {
        cancelled = true;                               // ***
        subscription.remove();
    };
}, []);

If you don't have any await s, it would appear that React Native has some race condition where it may still call your subscription callback even after you've done a remove on it, which would be less than ideal. The above will handle that, too.

Having a cancelled flag like that on its own is often considered an anti-pattern (better to cancel the subscription than just ignore the call), but that's not what we're doing above. We're cancelling the subscription and dealing with the possibility the code is called or continues after the subscription is cancelled, which is fine.

If you can't find a flag on EventSubscription , you might consider giving yourself a wrapper for it so you can reuse this easily:

function subscribeAppState(fn) {
    const subscription = AppState.subscribe(fn);
    return {
        remove() {
            subscription.remove();
            this.removed = true;
        }
    };
}

Then:

useEffect(() => {
    const sub = subscribeAppState(
        "change",
        async (nextAppState) => {
            if (sub.removed) {                          //
                return;                                 // ***
            }                                           //
            if (
                appState.current.match(/inactive|background/) &&
                nextAppState === "active"
            ) {
                setAppStateVisible(true); // set state to true
            }

            await something();                          // Example of an `await`
            if (sub.removed) {                          //
                return;                                 // ***
            }                                           //
    
            appState.current = nextAppState;
            setAppStateVisible(appState.current);
        }
    );

    return () => {
        sub.remove();
    };
}, []);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM