Warning to make a cleanup function in useEffect() occurs occasionally - reactjs

I am using AWS-Amplify and when a user signs out I get a warning:
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.
It mentions it occurs in the Profile.tsx file and this is the useEffect() hook.
But the issue is that the error occurs sometimes.
I keep testing it out and it comes and goes and I have no idea why it is so.
function Profile() {
const [user, setUser] = useState<IIntialState | null>(null);
useEffect(() => {
checkUser();
Hub.listen("auth", data => {
const { payload } = data;
if (payload.event === "signOut") {
setUser(null);
}
});
}, []);
async function checkUser() {
try {
const data = await Auth.currentUserPoolUser();
const userInfo = { username: data.username, ...data.attributes };
console.log(userInfo);
setUser(userInfo);
} catch (err) {
console.log("error: ", err);
}
}
function signOut() {
Auth.signOut().catch(err => console.log("error signing out: ", err));
}
if (user) {
return (
<Container>
<h1>Profile</h1>
<h2>Username: {user.username}</h2>
<h3>Email: {user.email}</h3>
<h4>Phone: {user.phone_number}</h4>
<Button onClick={signOut}>Sign Out</Button>
</Container>
);
}
return <Form setUser={setUser} />;
}

I happens because you component unmounts but you still have subscriptions in it.
React useEffect provides unmount function:
useEffect(() => {
Hub.listen("auth", func);
return () => {
// unsubscribe here
Hub.remove("auth", signOut)
};
});
And you Hub Class has remove method
remove(channel: string | RegExp, listener: HubCallback): void
Remove your subscription in useEffect return function
Hub.remove()

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.
The message is straightforward. We're trying to change the state of a component, even after it's been unmounted and unavailable.
There are multiple reasons why this can happen but the most common are that we didn’t unsubscribe to a websocket component, or this were dismount before an async operation finished. to solve this you can either do this :
useEffect(() => {
checkUser();
//check howto remove this listner in aws documentation
const listner= Hub.listen("auth", data => {
const { payload } = data;
if (payload.event === "signOut") {
setUser(null);
}
});
//this return function is called on component unmount
return ()=>{/* romve the listner here */}
}, []);
or go with this simple approach .
useEffect(() => {
let mounted =true
Hub.listen("auth", data => {
const { payload } = data;
if (payload.event === "signOut" && mounted ) {
setUser(null);
}
});
//this return function is called on component unmount
return ()=>{mounted =false }
}, []);
Read more about this here.

Related

Memory leak in react application

After clicking on submit I got this warning
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.
This is the code
const handleSubmit = async (e) => {
e.preventDefault()
let source = axios.CancelToken.source();
dispatch(login(email, password, source.token))
.then(() => {
console.log("Result from dispatch");
props.history.push("/Dashboard");//this is line which casues a warning.
window.location.reload();
})
.catch(() => {
setLoading(false);
});
}
How to avoid this warning? Any help would be appreciated.
When props.history.push('/Dashboard') is executed, your component gets unmounted, anything after you are trying to execute on this component (like a state update) would cause a memory leak.
I solved the warning by adding this variable mounded inside useEffect.
And I'm checking if a component is unmounted.
const mounted = useRef(false);
useEffect(() => {
mounted.current = true;
return () => { mounted.current = false; };
}, []);
const handleSubmit = async (e) => {
e.preventDefault()
let source = axios.CancelToken.source();
dispatch(login(email, password, source.token))
.then(() => {
if (!mounted) {
console.log("Result from dispatch");
props.history.push("/Dashboard");
window.location.reload();
}
})
.catch(() => {
setLoading(false);
});
}

useEffect: Can't perform a React state update on an unmounted component [duplicate]

When fetching data I'm getting: Can't perform a React state update on an unmounted component. The app still works, but react is suggesting I might be causing a memory leak.
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."
Why do I keep getting this warning?
I tried researching these solutions:
https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
https://developer.mozilla.org/en-US/docs/Web/API/AbortController
but this still was giving me the warning.
const ArtistProfile = props => {
const [artistData, setArtistData] = useState(null)
const token = props.spotifyAPI.user_token
const fetchData = () => {
const id = window.location.pathname.split("/").pop()
console.log(id)
props.spotifyAPI.getArtistProfile(id, ["album"], "US", 10)
.then(data => {setArtistData(data)})
}
useEffect(() => {
fetchData()
return () => { props.spotifyAPI.cancelRequest() }
}, [])
return (
<ArtistProfileContainer>
<AlbumContainer>
{artistData ? artistData.artistAlbums.items.map(album => {
return (
<AlbumTag
image={album.images[0].url}
name={album.name}
artists={album.artists}
key={album.id}
/>
)
})
: null}
</AlbumContainer>
</ArtistProfileContainer>
)
}
Edit:
In my api file I added an AbortController() and used a signal so I can cancel a request.
export function spotifyAPI() {
const controller = new AbortController()
const signal = controller.signal
// code ...
this.getArtist = (id) => {
return (
fetch(
`https://api.spotify.com/v1/artists/${id}`, {
headers: {"Authorization": "Bearer " + this.user_token}
}, {signal})
.then(response => {
return checkServerStat(response.status, response.json())
})
)
}
// code ...
// this is my cancel method
this.cancelRequest = () => controller.abort()
}
My spotify.getArtistProfile() looks like this
this.getArtistProfile = (id,includeGroups,market,limit,offset) => {
return Promise.all([
this.getArtist(id),
this.getArtistAlbums(id,includeGroups,market,limit,offset),
this.getArtistTopTracks(id,market)
])
.then(response => {
return ({
artist: response[0],
artistAlbums: response[1],
artistTopTracks: response[2]
})
})
}
but because my signal is used for individual api calls that are resolved in a Promise.all I can't abort() that promise so I will always be setting the state.
For me, clean the state in the unmount of the component helped.
const [state, setState] = useState({});
useEffect(() => {
myFunction();
return () => {
setState({}); // This worked for me
};
}, []);
const myFunction = () => {
setState({
name: 'Jhon',
surname: 'Doe',
})
}
Sharing the AbortController between the fetch() requests is the right approach.
When any of the Promises are aborted, Promise.all() will reject with AbortError:
function Component(props) {
const [fetched, setFetched] = React.useState(false);
React.useEffect(() => {
const ac = new AbortController();
Promise.all([
fetch('http://placekitten.com/1000/1000', {signal: ac.signal}),
fetch('http://placekitten.com/2000/2000', {signal: ac.signal})
]).then(() => setFetched(true))
.catch(ex => console.error(ex));
return () => ac.abort(); // Abort both fetches on unmount
}, []);
return fetched;
}
const main = document.querySelector('main');
ReactDOM.render(React.createElement(Component), main);
setTimeout(() => ReactDOM.unmountComponentAtNode(main), 1); // Unmount after 1ms
<script src="//cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.development.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.development.js"></script>
<main></main>
For example, you have some component that does some asynchronous actions, then writes the result to state and displays the state content on a page:
export default function MyComponent() {
const [loading, setLoading] = useState(false);
const [someData, setSomeData] = useState({});
// ...
useEffect( () => {
(async () => {
setLoading(true);
someResponse = await doVeryLongRequest(); // it takes some time
// When request is finished:
setSomeData(someResponse.data); // (1) write data to state
setLoading(false); // (2) write some value to state
})();
}, []);
return (
<div className={loading ? "loading" : ""}>
{someData}
<Link to="SOME_LOCAL_LINK">Go away from here!</Link>
</div>
);
}
Let's say that user clicks some link when doVeryLongRequest() still executes. MyComponent is unmounted but the request is still alive and when it gets a response it tries to set state in lines (1) and (2) and tries to change the appropriate nodes in HTML. We'll get an error from subject.
We can fix it by checking whether compponent is still mounted or not. Let's create a componentMounted ref (line (3) below) and set it true. When component is unmounted we'll set it to false (line (4) below). And let's check the componentMounted variable every time we try to set state (line (5) below).
The code with fixes:
export default function MyComponent() {
const [loading, setLoading] = useState(false);
const [someData, setSomeData] = useState({});
const componentMounted = useRef(true); // (3) component is mounted
// ...
useEffect( () => {
(async () => {
setLoading(true);
someResponse = await doVeryLongRequest(); // it takes some time
// When request is finished:
if (componentMounted.current){ // (5) is component still mounted?
setSomeData(someResponse.data); // (1) write data to state
setLoading(false); // (2) write some value to state
}
return () => { // This code runs when component is unmounted
componentMounted.current = false; // (4) set it to false when we leave the page
}
})();
}, []);
return (
<div className={loading ? "loading" : ""}>
{someData}
<Link to="SOME_LOCAL_LINK">Go away from here!</Link>
</div>
);
}
Why do I keep getting this warning?
The intention of this warning is to help you prevent memory leaks in your application. If the component updates it's state after it has been unmounted from the DOM, this is an indication that there could be a memory leak, but it is an indication with a lot of false positives.
How do I know if I have a memory leak?
You have a memory leak if an object that lives longer than your component holds a reference to it, either directly or indirectly. This usually happens when you subscribe to events or changes of some kind without unsubscribing when your component unmounts from the DOM.
It typically looks like this:
useEffect(() => {
function handleChange() {
setState(store.getState())
}
// "store" lives longer than the component,
// and will hold a reference to the handleChange function.
// Preventing the component to be garbage collected after
// unmount.
store.subscribe(handleChange)
// Uncomment the line below to avoid memory leak in your component
// return () => store.unsubscribe(handleChange)
}, [])
Where store is an object that lives further up the React tree (possibly in a context provider), or in global/module scope. Another example is subscribing to events:
useEffect(() => {
function handleScroll() {
setState(window.scrollY)
}
// document is an object in global scope, and will hold a reference
// to the handleScroll function, preventing garbage collection
document.addEventListener('scroll', handleScroll)
// Uncomment the line below to avoid memory leak in your component
// return () => document.removeEventListener(handleScroll)
}, [])
Another example worth remembering is the web API setInterval, which can also cause memory leak if you forget to call clearInterval when unmounting.
But that is not what I am doing, why should I care about this warning?
React's strategy to warn whenever state updates happen after your component has unmounted creates a lot of false positives. The most common I've seen is by setting state after an asynchronous network request:
async function handleSubmit() {
setPending(true)
await post('/someapi') // component might unmount while we're waiting
setPending(false)
}
You could technically argue that this also is a memory leak, since the component isn't released immediately after it is no longer needed. If your "post" takes a long time to complete, then it will take a long time to for the memory to be released. However, this is not something you should worry about, because it will be garbage collected eventually. In these cases, you could simply ignore the warning.
But it is so annoying to see the warning, how do I remove it?
There are a lot of blogs and answers on stackoverflow suggesting to keep track of the mounted state of your component and wrap your state updates in an if-statement:
let isMountedRef = useRef(false)
useEffect(() => {
isMountedRef.current = true
return () => {
isMountedRef.current = false
}
}, [])
async function handleSubmit() {
setPending(true)
await post('/someapi')
if (!isMountedRef.current) {
setPending(false)
}
}
This is not an recommended approach! Not only does it make the code less readable and adds runtime overhead, but it might also might not work well with future features of React. It also does nothing at all about the "memory leak", the component will still live just as long as without that extra code.
The recommended way to deal with this is to either cancel the asynchronous function (with for instance the AbortController API), or to ignore it.
In fact, React dev team recognises the fact that avoiding false positives is too difficult, and has removed the warning in v18 of React.
You can try this set a state like this and check if your component mounted or not. This way you are sure that if your component is unmounted you are not trying to fetch something.
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
return () => setDidMount(false);
}, [])
if(!didMount) {
return null;
}
return (
<ArtistProfileContainer>
<AlbumContainer>
{artistData ? artistData.artistAlbums.items.map(album => {
return (
<AlbumTag
image={album.images[0].url}
name={album.name}
artists={album.artists}
key={album.id}
/>
)
})
: null}
</AlbumContainer>
</ArtistProfileContainer>
)
Hope this will help you.
I had a similar issue with a scroll to top and #CalosVallejo answer solved it :) Thank you so much!!
const ScrollToTop = () => {
const [showScroll, setShowScroll] = useState();
//------------------ solution
useEffect(() => {
checkScrollTop();
return () => {
setShowScroll({}); // This worked for me
};
}, []);
//----------------- solution
const checkScrollTop = () => {
setShowScroll(true);
};
const scrollTop = () => {
window.scrollTo({ top: 0, behavior: "smooth" });
};
window.addEventListener("scroll", checkScrollTop);
return (
<React.Fragment>
<div className="back-to-top">
<h1
className="scrollTop"
onClick={scrollTop}
style={{ display: showScroll }}
>
{" "}
Back to top <span>⟶ </span>
</h1>
</div>
</React.Fragment>
);
};
I have getting same warning, This solution Worked for me ->
useEffect(() => {
const unsubscribe = fetchData(); //subscribe
return unsubscribe; //unsubscribe
}, []);
if you have more then one fetch function then
const getData = () => {
fetch1();
fetch2();
fetch3();
}
useEffect(() => {
const unsubscribe = getData(); //subscribe
return unsubscribe; //unsubscribe
}, []);
This error occurs when u perform state update on current component after navigating to other component:
for example
axios
.post(API.BASE_URI + API.LOGIN, { email: username, password: password })
.then((res) => {
if (res.status === 200) {
dispatch(login(res.data.data)); // line#5 logging user in
setSigningIn(false); // line#6 updating some state
} else {
setSigningIn(false);
ToastAndroid.show(
"Email or Password is not correct!",
ToastAndroid.LONG
);
}
})
In above case on line#5 I'm dispatching login action which in return navigates user to the dashboard and hence login screen now gets unmounted.
Now when React Native reaches as line#6 and see there is state being updated, it yells out loud that how do I do this, the login component is there no more.
Solution:
axios
.post(API.BASE_URI + API.LOGIN, { email: username, password: password })
.then((res) => {
if (res.status === 200) {
setSigningIn(false); // line#6 updating some state -- moved this line up
dispatch(login(res.data.data)); // line#5 logging user in
} else {
setSigningIn(false);
ToastAndroid.show(
"Email or Password is not correct!",
ToastAndroid.LONG
);
}
})
Just move react state update above, move line 6 up the line 5.
Now state is being updated before navigating the user away. WIN WIN
there are many answers but I thought I could demonstrate more simply how the abort works (at least how it fixed the issue for me):
useEffect(() => {
// get abortion variables
let abortController = new AbortController();
let aborted = abortController.signal.aborted; // true || false
async function fetchResults() {
let response = await fetch(`[WEBSITE LINK]`);
let data = await response.json();
aborted = abortController.signal.aborted; // before 'if' statement check again if aborted
if (aborted === false) {
// All your 'set states' inside this kind of 'if' statement
setState(data);
}
}
fetchResults();
return () => {
abortController.abort();
};
}, [])
Other Methods:
https://medium.com/wesionary-team/how-to-fix-memory-leak-issue-in-react-js-using-hook-a5ecbf9becf8
If the user navigates away, or something else causes the component to get destroyed before the async call comes back and tries to setState on it, it will cause the error. It's generally harmless if it is, indeed, a late-finish async call. There's a couple of ways to silence the error.
If you're implementing a hook like useAsync you can declare your useStates with let instead of const, and, in the destructor returned by useEffect, set the setState function(s) to a no-op function.
export function useAsync<T, F extends IUseAsyncGettor<T>>(gettor: F, ...rest: Parameters<F>): IUseAsync<T> {
let [parameters, setParameters] = useState(rest);
if (parameters !== rest && parameters.some((_, i) => parameters[i] !== rest[i]))
setParameters(rest);
const refresh: () => void = useCallback(() => {
const promise: Promise<T | void> = gettor
.apply(null, parameters)
.then(value => setTuple([value, { isLoading: false, promise, refresh, error: undefined }]))
.catch(error => setTuple([undefined, { isLoading: false, promise, refresh, error }]));
setTuple([undefined, { isLoading: true, promise, refresh, error: undefined }]);
return promise;
}, [gettor, parameters]);
useEffect(() => {
refresh();
// and for when async finishes after user navs away //////////
return () => { setTuple = setParameters = (() => undefined) }
}, [refresh]);
let [tuple, setTuple] = useState<IUseAsync<T>>([undefined, { isLoading: true, refresh, promise: Promise.resolve() }]);
return tuple;
}
That won't work well in a component, though. There, you can wrap useState in a function which tracks mounted/unmounted, and wraps the returned setState function with the if-check.
export const MyComponent = () => {
const [numPendingPromises, setNumPendingPromises] = useUnlessUnmounted(useState(0));
// ..etc.
// imported from elsewhere ////
export function useUnlessUnmounted<T>(useStateTuple: [val: T, setVal: Dispatch<SetStateAction<T>>]): [T, Dispatch<SetStateAction<T>>] {
const [val, setVal] = useStateTuple;
const [isMounted, setIsMounted] = useState(true);
useEffect(() => () => setIsMounted(false), []);
return [val, newVal => (isMounted ? setVal(newVal) : () => void 0)];
}
You could then create a useStateAsync hook to streamline a bit.
export function useStateAsync<T>(initialState: T | (() => T)): [T, Dispatch<SetStateAction<T>>] {
return useUnlessUnmounted(useState(initialState));
}
Try to add the dependencies in useEffect:
useEffect(() => {
fetchData()
return () => { props.spotifyAPI.cancelRequest() }
}, [fetchData, props.spotifyAPI])
Usually this problem occurs when you showing the component conditionally, for example:
showModal && <Modal onClose={toggleModal}/>
You can try to do some little tricks in the Modal onClose function, like
setTimeout(onClose, 0)
This works for me :')
const [state, setState] = useState({});
useEffect( async ()=>{
let data= await props.data; // data from API too
setState(users);
},[props.data]);
I had this problem in React Native iOS and fixed it by moving my setState call into a catch. See below:
Bad code (caused the error):
const signupHandler = async (email, password) => {
setLoading(true)
try {
const token = await createUser(email, password)
authContext.authenticate(token)
} catch (error) {
Alert.alert('Error', 'Could not create user.')
}
setLoading(false) // this line was OUTSIDE the catch call and triggered an error!
}
Good code (no error):
const signupHandler = async (email, password) => {
setLoading(true)
try {
const token = await createUser(email, password)
authContext.authenticate(token)
} catch (error) {
Alert.alert('Error', 'Could not create user.')
setLoading(false) // moving this line INTO the catch call resolved the error!
}
}
Similar problem with my app, I use a useEffect to fetch some data, and then update a state with that:
useEffect(() => {
const fetchUser = async() => {
const {
data: {
queryUser
},
} = await authFetch.get(`/auth/getUser?userId=${createdBy}`);
setBlogUser(queryUser);
};
fetchUser();
return () => {
setBlogUser(null);
};
}, [_id]);
This improves upon Carlos Vallejo's answer.
useEffect(() => {
let abortController = new AbortController();
// your async action is here
return () => {
abortController.abort();
}
}, []);
in the above code, I've used AbortController to unsubscribe the effect. When the a sync action is completed, then I abort the controller and unsubscribe the effect.
it work for me ....
The easy way
let fetchingFunction= async()=>{
// fetching
}
React.useEffect(() => {
fetchingFunction();
return () => {
fetchingFunction= null
}
}, [])
options={{
filterType: "checkbox"
,
textLabels: {
body: {
noMatch: isLoading ?
:
'Sorry, there is no matching data to display',
},
},
}}

React Memory Leakage with typescript using google firestore and react router

I am currently using firestore but I am currently getting problems with memory leakage as I delete transactionally delete my component just before I leave the screen what is the best way I to avoid this leakage When I click on the link component I want to remove the notification from the database it seems to work and take me to the new page the only problem is that I get a memory leak how should I avoid this.
const loadAlerts = useCallback(() => {
const alertNotifcationsObserver = onSnapshot(notifications, (querySnapshot) => {
const alertData: any[] = []
querySnapshot.forEach((doc) => {
console.log(doc.data())
alertData.push({
...doc.data(),
doc
})
});
setAlertNotifcations(alertData)
});
return alertNotifcationsObserver
}, [notifications])
useEffect(() => {
loadAlerts();
console.log(alertNotifications)
}, []);
<IonItem onClick={async (e) => {
console.log(i.Reference)
await notificationsController(i.Reference)
}} key={i.Reference} lines='full'>
<Link
to={{
pathname: i.Link,
state: { documentReferencePath: i.Reference }
}}
>
{i.Type}
</Link>
</IonItem>
const notificationsController = async(documentReferencePath:string)=>{
try {
await runTransaction(db, async (transaction) => {
const documentReference = doc(db,documentReferencePath)
const notificationDoc = await transaction.get(documentReference);
if (!notificationDoc.exists()) {
throw "Document does not exist!";
}
transaction.delete(documentReference)
});
console.log("Notification removed successfully committed!");
} catch (e) {
console.log("Transaction failed: ", e);
}
index.js:1 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.
How do I get rid of this error also I tried to add unsubscribe for loadAlerts but type script gives me a error too.
Argument of type '() => () => Unsubscribe' is not assignable to parameter of type 'EffectCallback'.
It seems unlikely you'll call the loadAlerts function outside the useEffect hook or in any other callback, move it into the useEffect hook to remove it as a dependency, and return a cleanup function from the useEffect to unsubscribe.
useEffect(() => {
const unsubscribe = onSnapshot(
notifications,
(querySnapshot) => {
const alertData: any[];
querySnapshot.forEach((doc) => {
alertData.push({
...doc.data(),
doc,
});
});
setAlertNotifcations(alertData);
});
return unsubscribe;
}, []);

amplify dynamodb graphql fetch function is running infinite in loop

I am trying to fetch records from dynamodb using graphql (AWS amplify reacjs setup). I have created a component CTCards and using this in App.js
The problem that we are facing is function fetchTodos is running in infinite loop though in Database there is only two records and I am getting exactly 2 records but this function is running in infinite loop
When I comment the fetching code then the program runs as expected
//== CTCards ( component which is giving issue) ===
function CTCards(props) {
const [todo, setTodos] = useState([]);
useEffect(() => {
fetchTodos();
}, []);
const fetchTodos = async () => {
try {
// == this-block ===
const todoData = await API.graphql(graphqlOperation(listTodos));
const todoList = todoData.data.listTodos.items;
console.log('To Do List', todoList);
setTodos(todoList);
// == /this-block ===
} catch (error) {
console.log('error on fetching to do list', error);
}
};
return (
<div style={{color: "red"}}>
{todo.map((todo, index) => {
<div style={{color: "red"}}>
<div>{todo.name}</div>
<div>{todo.description}</div>
</div>
})}
</div>
)
}
//== App.js code ===
function App() {
return (
<div>
<CTCards/>
</div>
);
}
The setTodos is remounting the component so the useEffect body runs again.
If you comment out the setTodos it should run normally.
You can use React.memo to compare props and not the virtual dom but it will still do a useless fetch. Consider restructuring your application so the fetch happens one layer above.
As #arti91 said it s cause you are updating the state and doing so react rerenders the component and runs the useEffect hook again.
A possible solution to this could be to check inside the hook if you already fetched the data.
useEffect(() => {
if (!todos) {
fetchTodos();
}
}, []);
Instead of this code:
try {
// == this-block ===
const todoData = await API.graphql(graphqlOperation(listTodos));
const todoList = todoData.data.listTodos.items;
console.log('To Do List', todoList);
setTodos(todoList);
// == /this-block ===
} catch (error) {
console.log('error on fetching to do list', error);
}
Try this code:
try {
const todoData = API.graphql({ query: queries.listTodos }).then(response => {
setTodos(response.data.listTodos.items);
}).catch(err => {
console.log(err);
});
} catch (err) {
console.log('error facing Todos:', err)
}
This issue occurs because the of the Promise that is still is prebuild phase so if we give it some time that it could complete the cycle and get the value to store it.

What is the right way to cancel all async/await tasks within an useEffect hook to prevent memory leaks in react?

I am working on a react chap app that pulls data from a firebase database. In my "Dashboard" component I have an useEffect hook checking for an authenticated user and if so, pull data from firebase and set the state of a an email variable and chats variable. I use abortController for my useEffect cleanup, however whenever I first log out and log back in I get a memory leak warning.
index.js:1375 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 Dashboard (created by Context.Consumer)
Originally I didn't have the abortController, I just returned a console log on clean up. Did more research and found abortController however the examples use fetch and signal and I could not find any resources on using with async/await. I am open to changing how the data is retrieved, (whether that is with fetch, async/await, or any other solution) I just have not been able to get it working with the other methods.
const [email, setEmail] = useState(null);
const [chats, setChats] = useState([]);
const signOut = () => {
firebase.auth().signOut();
};
useEffect(() => {
const abortController = new AbortController();
firebase.auth().onAuthStateChanged(async _user => {
if (!_user) {
history.push('/login');
} else {
await firebase
.firestore()
.collection('chats')
.where('users', 'array-contains', _user.email)
.onSnapshot(async res => {
const chatsMap = res.docs.map(_doc => _doc.data());
console.log('res:', res.docs);
await setEmail(_user.email);
await setChats(chatsMap);
});
}
});
return () => {
abortController.abort();
console.log('aborting...');
};
}, [history, setEmail, setChats]);
Expected result is to properly cleanup/cancel all asynchronous tasks in a useEffect cleanup function. After one user logs out then either the same or different user log back in I get the following warning in the console
index.js:1375 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 Dashboard (created by Context.Consumer)
In the case of firebase you aren't dealing with async/await but streams. You should just unsubscribe from firebase streams in cleanup function:
const [email, setEmail] = useState(null);
const [chats, setChats] = useState([]);
const signOut = () => {
firebase.auth().signOut();
};
useEffect(() => {
let unsubscribeSnapshot;
const unsubscribeAuth = firebase.auth().onAuthStateChanged(_user => {
// you're not dealing with promises but streams so async/await is not needed here
if (!_user) {
history.push('/login');
} else {
unsubscribeSnapshot = firebase
.firestore()
.collection('chats')
.where('users', 'array-contains', _user.email)
.onSnapshot(res => {
const chatsMap = res.docs.map(_doc => _doc.data());
console.log('res:', res.docs);
setEmail(_user.email);
setChats(chatsMap);
});
}
});
return () => {
unsubscribeAuth();
unsubscribeSnapshot && unsubscribeSnapshot();
};
}, [history]); // setters are stable between renders so you don't have to put them here
The onSnapshot method does not return a promise, so there's no sense in awaiting its result. Instead it starts listening for the data (and changes to that data), and calls the onSnapshot callback with the relevant data. This can happen multiple times, hence it can't return a promise. The listener stays attached to the database until you unsubscribe it by calling the method that is returned from onSnapshot. Since you never store that method, let alone call it, the listener stays active, and will later again call your callback. This is likely where the memory leak comes from.
If you want to wait for the result from Firestore, you're probably looking for the get() method. This gets the data once, and then resolves the promise.
await firebase
.firestore()
.collection('chats')
.where('users', 'array-contains', _user.email)
.get(async res => {
One way to cancel async/await is to create something like built-in AbortController that will return two functions: one for cancelling and one for checking for cancelation, and then before each step in async/await a check for cancellation needs to be run:
function $AbortController() {
let res, rej;
const p = new Promise((resolve, reject) => {
res = resolve;
rej = () => reject($AbortController.cSymbol);
})
function isCanceled() {
return Promise.race([p, Promise.resolve()]);
}
return [
rej,
isCanceled
];
}
$AbortController.cSymbol = Symbol("cancel");
function delay(t) {
return new Promise((res) => {
setTimeout(res, t);
})
}
let cancel, isCanceled;
document.getElementById("start-logging").addEventListener("click", async (e) => {
try {
cancel && cancel();
[cancel, isCanceled] = $AbortController();
const lisCanceled = isCanceled;
while(true) {
await lisCanceled(); // check for cancellation
document.getElementById("container").insertAdjacentHTML("beforeend", `<p>${Date.now()}</p>`);
await delay(2000);
}
} catch (e) {
if(e === $AbortController.cSymbol) {
console.log("cancelled");
}
}
})
document.getElementById("cancel-logging").addEventListener("click", () => cancel())
<button id="start-logging">start logging</button>
<button id="cancel-logging">cancel logging</button>
<div id="container"></div>

Resources