This document describes the memory leak fixes implemented in the ArenaX frontend application to prevent memory accumulation when components are removed from the DOM.
- Location:
frontend/src/hooks/useKeyboardShortcuts.ts - Issue: The chord timer (
chordTimerRef) was not being cleared on component unmount, causing potential memory leaks - Impact: Timer references remained in memory after component unmount
- Location:
frontend/src/messages/useRealtimeMessages.ts - Issue: WebSocket connections were not being disconnected on component unmount
- Impact: Open WebSocket connections persisted after component unmount, causing memory accumulation
- Location:
frontend/src/hooks/usePushNotifications.ts - Issue: Push notification subscriptions were not being cleaned up on component unmount
- Impact: Service worker subscriptions remained active after component unmount
- Location:
frontend/src/components/ui/VirtualDynamicList.tsx - Issue: ResizeObserver instances were not being properly cleaned up when items changed or component unmounted
- Impact: Observer references accumulated in memory
Before:
useEffect(() => {
const handler = (event: KeyboardEvent) => {
// ... handler logic
};
window.addEventListener("keydown", handler, { capture: true });
return () => window.removeEventListener("keydown", handler, { capture: true });
}, [allShortcuts, effectiveKey, fire]);After:
useEffect(() => {
const handler = (event: KeyboardEvent) => {
// ... handler logic
};
window.addEventListener("keydown", handler, { capture: true });
return () => {
window.removeEventListener("keydown", handler, { capture: true });
// Clean up chord timer on unmount
if (chordTimerRef.current) {
clearTimeout(chordTimerRef.current);
chordTimerRef.current = null;
}
pendingChordRef.current = null;
};
}, [allShortcuts, effectiveKey, fire]);Before:
useEffect(() => {
if (!resolvedUrl || typeof window === "undefined") return;
const manager = getOrCreateManager(resolvedUrl, getToken);
managerRef.current = manager;
if (autoConnect) {
manager.connect();
}
return () => {
// Don't disconnect on unmount — the manager is shared across conversations
// Actual disconnect happens when explicitly called or page unloads
};
}, [resolvedUrl, autoConnect, getToken]);After:
useEffect(() => {
if (!resolvedUrl || typeof window === "undefined") return;
const manager = getOrCreateManager(resolvedUrl, getToken);
managerRef.current = manager;
if (autoConnect) {
manager.connect();
}
return () => {
// Disconnect on unmount to prevent memory leaks
// The manager is shared but should be cleaned up when no longer needed
if (managerRef.current) {
managerRef.current.disconnect();
}
};
}, [resolvedUrl, autoConnect, getToken]);Before:
useEffect(() => {
const checkPermission = async () => {
// ... permission check logic
};
checkPermission();
}, []);After:
useEffect(() => {
const checkPermission = async () => {
// ... permission check logic
};
checkPermission();
// Cleanup: unsubscribe on unmount to prevent memory leaks
return () => {
unsubscribe().catch((error) => {
console.error("[PushNotifications] Cleanup unsubscribe error:", error);
});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);Before:
useEffect(() => {
const observers = observersRef.current;
return () => {
Object.values(observers).forEach((o) => o.disconnect());
};
}, []);After:
useEffect(() => {
const observers = observersRef.current;
return () => {
Object.values(observers).forEach((o) => o.disconnect());
// Clear the observers reference to prevent memory leaks
Object.keys(observersRef.current).forEach((key) => {
delete observersRef.current[key];
});
};
}, [items]);A comprehensive memory leak test suite has been added at frontend/src/__tests__/memory-leak.test.ts to verify:
-
useKeyboardShortcuts
- Chord timer cleanup on unmount
- Event listener removal on unmount
- Rapid mount/unmount cycles
-
useInterval
- Interval clearing on unmount
- Timeout clearing on unmount
-
useMobile
- Resize event listener cleanup
- Connection event listener cleanup
-
useResponsive
- ResizeObserver disconnection
- Resize event listener removal
-
useRealtimeMessages
- WebSocket disconnection on unmount
-
Multiple Mount/Unmount Cycles
- Rapid component mounting/unmounting without memory buildup
cd frontend
npm test -- memory-leak.test.tsEvery useEffect that creates resources should return a cleanup function:
useEffect(() => {
const resource = createResource();
return () => resource.cleanup();
}, []);Always clear timers and intervals in cleanup functions:
useEffect(() => {
const timer = setTimeout(() => {}, 1000);
const interval = setInterval(() => {}, 1000);
return () => {
clearTimeout(timer);
clearInterval(interval);
};
}, []);Always remove event listeners that were added:
useEffect(() => {
const handler = () => {};
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);Always disconnect observers (ResizeObserver, IntersectionObserver, MutationObserver):
useEffect(() => {
const observer = new ResizeObserver(() => {});
observer.observe(element);
return () => observer.disconnect();
}, []);Always unsubscribe from subscriptions:
useEffect(() => {
const subscription = observable.subscribe();
return () => subscription.unsubscribe();
}, []);Memory usage can be monitored using browser DevTools:
- Open Chrome DevTools (F12)
- Go to the "Memory" tab
- Take a heap snapshot before and after component interactions
- Compare snapshots to identify memory leaks
- useKeyboardShortcuts chord timer cleanup
- useRealtimeMessages WebSocket cleanup
- usePushNotifications subscription cleanup
- VirtualDynamicList observer cleanup
- Memory leak test suite created
- Documentation created
- Memory monitoring implementation (future enhancement)
- Automatic cleanup hooks (future enhancement)
- Memory leak detection (future enhancement)
Create a custom hook to monitor memory usage in development:
function useMemoryMonitor() {
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
const interval = setInterval(() => {
if (performance.memory) {
console.log('Memory usage:', performance.memory);
}
}, 5000);
return () => clearInterval(interval);
}
}, []);
}Implement ESLint rules to detect missing cleanup functions in useEffect hooks.
Add runtime detection for common memory leak patterns.
- React useEffect Cleanup: https://react.dev/reference/react/useEffect#cleaning-up-an-effect
- Chrome DevTools Memory Profiling: https://developer.chrome.com/docs/devtools/memory-problems/
- MDN Web Workers: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API