Bug Description
In src/context/AuthContext.jsx, the useEffect that listens for the rh_avatar_updated localStorage event registers and attempts to remove the listener using two different anonymous arrow function instances:
// line 236 — registers reference A
window.addEventListener("storage", e => handleStorageChange(e));
// line 237 — tries to remove reference B (a brand-new function, never registered)
return () => window.removeEventListener("storage", e => handleStorageChange(e));
removeEventListener requires the exact same function object that was passed to addEventListener. Since e => handleStorageChange(e) is evaluated twice and produces two distinct closures, the cleanup is a no-op. The listener from line 236 is never actually removed.
Impact
- In React Strict Mode (dev), effects run twice — leaving 2 dangling
storage listeners on window after each mount cycle
- The lingering listener holds a closure reference to
setUserData, keeping the component fiber subtree in memory after unmount
- On multi-tab avatar updates,
setUserData can be called on an already-unmounted component
Fix
Move handleStorageChange above the addEventListener call so the same reference is used in both places:
useEffect(() => {
const handleStorageChange = (e) => {
if (e.key !== "rh_avatar_updated") return;
try {
const payload = JSON.parse(e.newValue);
if (!payload || payload.uid !== auth?.currentUser?.uid) return;
setUserData(prev => prev ? { ...prev, avatar: payload.avatar } : prev);
} catch { /* ignore */ }
};
window.addEventListener("storage", handleStorageChange);
return () => window.removeEventListener("storage", handleStorageChange);
}, []);
You can verify the leak in DevTools: open Application > Event Listeners > window > storage — multiple entries will be registered after the component mounts.
Bug Description
In
src/context/AuthContext.jsx, theuseEffectthat listens for therh_avatar_updatedlocalStorage event registers and attempts to remove the listener using two different anonymous arrow function instances:removeEventListenerrequires the exact same function object that was passed toaddEventListener. Sincee => handleStorageChange(e)is evaluated twice and produces two distinct closures, the cleanup is a no-op. The listener from line 236 is never actually removed.Impact
storagelisteners onwindowafter each mount cyclesetUserData, keeping the component fiber subtree in memory after unmountsetUserDatacan be called on an already-unmounted componentFix
Move
handleStorageChangeabove theaddEventListenercall so the same reference is used in both places:You can verify the leak in DevTools: open Application > Event Listeners >
window>storage— multiple entries will be registered after the component mounts.