Toaster leaks matchMedia change listeners when theme prop changes or on unmount
In src/index.tsx, the effect that syncs theme="system" with prefers-color-scheme never cleans up its listener:
const darkMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
try {
darkMediaQuery.addEventListener('change', ({ matches }) => {
if (matches) setActualTheme('dark');
else setActualTheme('light');
});
} catch (error) {
darkMediaQuery.addListener(({ matches }) => { ... });
}The effect has [theme] as its dependency array, so every time theme changes a new MediaQueryList object gets created via window.matchMedia(...) and a new listener gets attached to it, but nothing removes the previous listener, and nothing removes any of them on unmount either. In apps that bind theme to a live toggle (<Toaster theme={theme} />), this accumulates a new leaked closure over setActualTheme every time the theme changes, for the whole lifetime of the page.
I ran a quick check locally: mounting <Toaster theme={theme} /> and toggling theme between light/dark/system a few times keeps stacking new change listeners on new MediaQueryList instances, none of which ever get released, confirmed with getEventListeners() in devtools.
Same underlying class of bug as the one fixed in hooks.tsx's useIsDocumentHidden in #711 (listener registered without matching removal), just a different effect that fix didn't touch.
Expected: the effect should keep a reference to the handler and return a cleanup function that calls removeEventListener (and removeListener in the Safari fallback branch), the same pattern #711 already applies to the visibility listener.
Source: emilkowalski/sonner