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();
};
}, []);
CodePudding user response:
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 Internet 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).
CodePudding user response:
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.
You can do that by adding 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.