You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[system][cssVars] useCurrentColorScheme writes storage-event values back to localStorage, causing a cross-tab echo loop #49244
Open any app using CssVarsProvider / ThemeProvider with colorSchemes: { light, dark } (e.g. the MUI docs) in 5 or more tabs.
In one tab, toggle the mode twice quickly, or run localStorage.setItem('mui-mode','dark'); localStorage.setItem('mui-mode','light').
Watch mui-mode in the other tabs: it flips back and forth and settles unpredictably.
Deterministic version: render N providers, each with its own storageWindow sharing one Map-backed localStorage, and dispatch storage events only to the other windows. After two setMode calls in quick succession, the echo writes grow without bound (we hit a 2,000-event cap with 9 simulated tabs).
Current behavior
In useCurrentColorScheme, the modeStorage.subscribe handler calls setMode(value), and setMode's setState updater calls modeStorage.set(newMode). setColorScheme does the same for the light/dark keys. So a value another tab wrote is written back by every receiving tab, at render time. If storage has changed again by then, the write-back is stale and fires storage in every other tab, which repeat the same thing. With N tabs and two changes in flight, it does not converge, and can settle on a value the user did not pick last.
Also: removing a key is written back as the default by every receiving tab.
Expected behavior
Values received through the storage event update state only; they are never persisted again.
Context
Suggested fix: split state update from persistence, e.g. an internal setMode(mode, { persist: false }) used by the subscriber (or have the subscriber call setState without modeStorage.set), and the same for setColorScheme.
Workaround we shipped: a custom storageManager whose subscribe listener applies the value storage currently holds (not event.newValue), synchronously (flushSync), with set suppressed while applying it. A simpler "skip values received through subscribe" match is not enough: it still loops on A→B→A bursts, lets stale events win, and writes removals back.
Your environment
@mui/system 7.3.11; the same useCurrentColorScheme code is present through 9.4.0 (also 9.0.0-beta.1). React 19. Reproduced in Chrome with multiple tabs and in jsdom with simulated windows.
This issue has been closed. If you have a similar problem but not exactly the same, please open a new issue.
Now, if you have additional information related to this issue or things that could help future readers, feel free to leave a comment.
@danemil Thanks again for creating this issue! If you have a moment, we would love to hear your thoughts on how we handled it with this short feedback form.
Activity
minwookshin commentedon Oct 1, 2026
I’m working on keeping subscription updates state-only, with regression coverage for rapid cross-tab changes and removed storage keys.
ZeeshanTamboli commentedon Oct 9, 2026
Why do you want to do this in the first place? Fixing this would be an overkill.
github-actions commentedon Oct 9, 2026
This issue has been closed. If you have a similar problem but not exactly the same, please open a new issue.
Now, if you have additional information related to this issue or things that could help future readers, feel free to leave a comment.
@danemil Thanks again for creating this issue! If you have a moment, we would love to hear your thoughts on how we handled it with this short feedback form.