const log = [];
let mounted = true;
function setData(value) {
if (!mounted) {
log.push('ERROR: setState after unmount');
return;
}
log.push('setState: ' + value);
}
// Simulates useEffect that fetches without cleanup
async function simulateEffect() {
const data = await Promise.resolve('user data');
setData(data); // might be called after unmount!
}
simulateEffect(); // start fetch
// Component unmounts before fetch completes
mounted = false;
// But the async code still runs and calls setState
setTimeout(() => console.log(log.join(' | ')), 0);const log = [];
let mounted = true;
function setData(value) {
if (!mounted) {
log.push('ERROR: setState after unmount');
return;
}
log.push('setState: ' + value);
}
// Fix: cancelled flag — useEffect cleanup sets it to true
async function simulateEffect() {
let cancelled = false;
const cleanup = () => { cancelled = true; };
const data = await Promise.resolve('user data');
if (!cancelled) {
setData(data); // only update if still mounted
} else {
log.push('fetch cancelled — component unmounted');
}
return cleanup;
}
const getCleanup = simulateEffect();
// Component unmounts
mounted = false;
getCleanup.then(cleanup => cleanup());
setTimeout(() => console.log(log.join(' | ')), 0);Bug: The async fetch resolves after unmount. setState is called on an unmounted component — memory leak and potential errors.
Explanation: The cleanup function sets cancelled = true. When the fetch resolves, it checks cancelled before calling setState.
Key Insight: Pattern for async useEffect: let cancelled = false; return () => { cancelled = true; }. Check !cancelled before every setState in the async callback.