const subscribers = new Set();
const log = [];
function subscribe(handler) {
subscribers.add(handler);
return () => subscribers.delete(handler);
}
function publish(value) {
subscribers.forEach(h => h(value));
}
// Simulates useEffect without cleanup
function simulateComponent(renders) {
for (let i = 0; i < renders; i++) {
// No cleanup — adds new subscriber on every render
subscribe(v => log.push('render' + i + ':' + v));
}
}
simulateComponent(3); // 3 renders
publish('hello');
console.log(log.length); // how many times was hello received?
console.log(log.join(','));const subscribers = new Set();
const log = [];
function subscribe(handler) {
subscribers.add(handler);
return () => subscribers.delete(handler);
}
function publish(value) {
subscribers.forEach(h => h(value));
}
// Simulates useEffect WITH cleanup
function simulateComponent(renders) {
let unsubscribe = null;
for (let i = 0; i < renders; i++) {
if (unsubscribe) unsubscribe(); // cleanup previous
unsubscribe = subscribe(v => log.push('handler:' + v));
}
}
simulateComponent(3); // 3 renders, but only 1 active subscriber
publish('hello');
console.log(log.length);
console.log(log[0]);Bug: No cleanup function returned. Each render adds a new subscription. After 3 renders, there are 3 active subscribers — 'hello' is received 3 times.
Explanation: Each render cleans up the previous subscription before adding a new one. After 3 renders, only the latest subscription is active.
Key Insight: Always return a cleanup function from useEffect when subscribing to any source (WebSocket, EventEmitter, store). Missing cleanup = memory leak + stale handlers receiving events after component unmounts.