Skip to content

[system][cssVars] useCurrentColorScheme writes storage-event values back to localStorage, causing a cross-tab echo loop #49244

Closed

Description

@danemil

Steps to reproduce

  1. Open any app using CssVarsProvider / ThemeProvider with colorSchemes: { light, dark } (e.g. the MUI docs) in 5 or more tabs.
  2. In one tab, toggle the mode twice quickly, or run localStorage.setItem('mui-mode','dark'); localStorage.setItem('mui-mode','light').
  3. 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.

Search keywords: useCurrentColorScheme storage event cross-tab loop localStorage mui-mode

Search keywords:

Activity

  1. added
    scope: systemThe system, the design tokens / styling foundations used across components. eg. @mui/system with MUI
    on Sep 29, 2026
  2. minwookshin commented on Oct 1, 2026

    @minwookshin
    Contributor

    I’m working on keeping subscription updates state-only, with regression coverage for rapid cross-tab changes and removed storage keys.

  3. ZeeshanTamboli commented on Oct 9, 2026

    @ZeeshanTamboli
    Member

    In one tab, toggle the mode twice quickly,

    Why do you want to do this in the first place? Fixing this would be an overkill.

  4. github-actions commented on Oct 9, 2026

    @github-actions
    Contributor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    scope: systemThe system, the design tokens / styling foundations used across components. eg. @mui/system with MUI

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions