问题
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)
回答1:
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
回答2:
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 => {
回答3:
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>
来源:https://stackoverflow.com/questions/58562474/what-is-the-right-way-to-cancel-all-async-await-tasks-within-an-useeffect-hook-t