diff --git a/docs/data/material/customization/dark-mode/dark-mode.md b/docs/data/material/customization/dark-mode/dark-mode.md index 248750d01716ad..ce0eb9e7f793b0 100644 --- a/docs/data/material/customization/dark-mode/dark-mode.md +++ b/docs/data/material/customization/dark-mode/dark-mode.md @@ -122,6 +122,78 @@ The `mode` is always `undefined` on first render, so make sure to handle this ca {{"demo": "ToggleColorMode.js", "defaultCodeOpen": false}} +## Storage manager + +By default, the [built-in support](#built-in-support) for color schemes uses the browser's `localStorage` API to store the user's mode and scheme preference. + +To use a different storage manager, create a custom function with this signature: + +```ts +type Unsubscribe = () => void; + +function storageManager(params: { key: string }): { + get: (defaultValue: any) => any; + set: (value: any) => void; + subscribe: (handler: (value: any) => void) => Unsubscribe; +}; +``` + +Then pass it to the `storageManager` prop of the `ThemeProvider` component: + +```tsx +import { ThemeProvider, createTheme } from '@mui/material/styles'; +import type { StorageManager } from '@mui/material/styles'; + +const theme = createTheme({ + colorSchemes: { + dark: true, + }, +}); + +function storageManager(params): StorageManager { + return { + get: (defaultValue) => { + // Your implementation + }, + set: (value) => { + // Your implementation + }, + subscribe: (handler) => { + // Your implementation + return () => { + // cleanup + }; + }, + }; +} + +function App() { + return ( + + ... + + ); +} +``` + +:::warning +If you are using the `InitColorSchemeScript` component to [prevent SSR flickering](/material-ui/customization/css-theme-variables/configuration/#preventing-ssr-flickering), you have to include the `localStorage` implementation in your custom storage manager. +::: + +### Disable storage + +To disable the storage manager, pass `null` to the `storageManager` prop: + +```tsx + + ... + +``` + +:::warning +Disabling the storage manager will cause the app to reset to its default mode whenever the user refreshes the page. +::: + ## Disable transitions To instantly switch between color schemes with no transition, apply the `disableTransitionOnChange` prop to the `ThemeProvider` component: diff --git a/packages/mui-material/src/styles/ThemeProvider.test.tsx b/packages/mui-material/src/styles/ThemeProvider.test.tsx index b5828595030b49..33a77b7e3c2642 100644 --- a/packages/mui-material/src/styles/ThemeProvider.test.tsx +++ b/packages/mui-material/src/styles/ThemeProvider.test.tsx @@ -12,7 +12,7 @@ describe('ThemeProvider', () => { originalMatchmedia = window.matchMedia; // Create mocks of localStorage getItem and setItem functions storage = {}; - Object.defineProperty(global, 'localStorage', { + Object.defineProperty(window, 'localStorage', { value: { getItem: (key: string) => storage[key], setItem: (key: string, value: string) => { diff --git a/packages/mui-material/src/styles/ThemeProvider.tsx b/packages/mui-material/src/styles/ThemeProvider.tsx index e9e0dc5fb94c82..25a7f013f43308 100644 --- a/packages/mui-material/src/styles/ThemeProvider.tsx +++ b/packages/mui-material/src/styles/ThemeProvider.tsx @@ -1,6 +1,7 @@ 'use client'; import * as React from 'react'; import { DefaultTheme } from '@mui/system'; +import { StorageManager } from '@mui/system/cssVars'; import ThemeProviderNoVars from './ThemeProviderNoVars'; import { CssThemeVariables } from './createThemeNoVars'; import { CssVarsProvider } from './ThemeProviderWithVars'; @@ -47,6 +48,11 @@ export interface ThemeProviderProps extends ThemeProviderC * @default window */ storageWindow?: Window | null; + /** + * The storage manager to be used for storing the mode and color scheme + * @default using `window.localStorage` + */ + storageManager?: StorageManager | null; /** * localStorage key used to store application `mode` * @default 'mui-mode' diff --git a/packages/mui-material/src/styles/ThemeProviderWithVars.spec.tsx b/packages/mui-material/src/styles/ThemeProviderWithVars.spec.tsx index ebd779e2085610..0f879463a8c0ca 100644 --- a/packages/mui-material/src/styles/ThemeProviderWithVars.spec.tsx +++ b/packages/mui-material/src/styles/ThemeProviderWithVars.spec.tsx @@ -1,5 +1,12 @@ import * as React from 'react'; -import { extendTheme, CssVarsProvider, styled, useTheme, Overlays } from '@mui/material/styles'; +import { + extendTheme, + ThemeProvider, + styled, + useTheme, + Overlays, + StorageManager, +} from '@mui/material/styles'; import type {} from '@mui/material/themeCssVarsAugmentation'; const customTheme = extendTheme({ @@ -53,7 +60,7 @@ function TestUseTheme() { return
test
; } - + ({ // test that `theme` in sx has access to CSS vars @@ -63,4 +70,16 @@ function TestUseTheme() { }, })} /> -; +; + +; + +const storageManager: StorageManager = () => { + return { + get: () => 'light', + set: () => {}, + subscribe: () => () => {}, + }; +}; + +; diff --git a/packages/mui-material/src/styles/index.d.ts b/packages/mui-material/src/styles/index.d.ts index 362e18e8759271..672e5d3342e8c5 100644 --- a/packages/mui-material/src/styles/index.d.ts +++ b/packages/mui-material/src/styles/index.d.ts @@ -98,6 +98,7 @@ export { default as withStyles } from './withStyles'; export { default as withTheme } from './withTheme'; export * from './ThemeProviderWithVars'; +export type { StorageManager } from '@mui/system/cssVars'; export { default as extendTheme } from './createThemeWithVars'; diff --git a/packages/mui-system/src/cssVars/createCssVarsProvider.d.ts b/packages/mui-system/src/cssVars/createCssVarsProvider.d.ts index 5dee405542060e..0b78935613dce0 100644 --- a/packages/mui-system/src/cssVars/createCssVarsProvider.d.ts +++ b/packages/mui-system/src/cssVars/createCssVarsProvider.d.ts @@ -1,6 +1,7 @@ import * as React from 'react'; import InitColorSchemeScript from '../InitColorSchemeScript'; import { Result } from './useCurrentColorScheme'; +import type { StorageManager } from './localStorageManager'; export interface ColorSchemeContextValue extends Result { @@ -70,6 +71,11 @@ export interface CreateCssVarsProviderResult< * @default document */ colorSchemeNode?: Element | null; + /** + * The storage manager to be used for storing the mode and color scheme. + * @default using `window.localStorage` + */ + storageManager?: StorageManager | null; /** * The window that attaches the 'storage' event listener * @default window diff --git a/packages/mui-system/src/cssVars/createCssVarsProvider.js b/packages/mui-system/src/cssVars/createCssVarsProvider.js index 3946722bd4574d..fa9246284c414d 100644 --- a/packages/mui-system/src/cssVars/createCssVarsProvider.js +++ b/packages/mui-system/src/cssVars/createCssVarsProvider.js @@ -60,6 +60,7 @@ export default function createCssVarsProvider(options) { modeStorageKey = defaultModeStorageKey, colorSchemeStorageKey = defaultColorSchemeStorageKey, disableTransitionOnChange = designSystemTransitionOnChange, + storageManager, storageWindow = typeof window === 'undefined' ? undefined : window, documentNode = typeof document === 'undefined' ? undefined : document, colorSchemeNode = typeof document === 'undefined' ? undefined : document.documentElement, @@ -119,6 +120,7 @@ export default function createCssVarsProvider(options) { modeStorageKey, colorSchemeStorageKey, defaultMode, + storageManager, storageWindow, noSsr, }); @@ -357,6 +359,11 @@ export default function createCssVarsProvider(options) { * You should use this option in conjuction with `InitColorSchemeScript` component. */ noSsr: PropTypes.bool, + /** + * The storage manager to be used for storing the mode and color scheme + * @default using `window.localStorage` + */ + storageManager: PropTypes.func, /** * The window that attaches the 'storage' event listener. * @default window diff --git a/packages/mui-system/src/cssVars/createCssVarsProvider.test.js b/packages/mui-system/src/cssVars/createCssVarsProvider.test.js index 4accde568ba8ca..9c699b7f1ff7ee 100644 --- a/packages/mui-system/src/cssVars/createCssVarsProvider.test.js +++ b/packages/mui-system/src/cssVars/createCssVarsProvider.test.js @@ -28,7 +28,7 @@ describe('createCssVarsProvider', () => { originalMatchmedia = window.matchMedia; // Create mocks of localStorage getItem and setItem functions - Object.defineProperty(global, 'localStorage', { + Object.defineProperty(window, 'localStorage', { value: { getItem: spy((key) => storage[key]), setItem: spy((key, value) => { @@ -584,13 +584,9 @@ describe('createCssVarsProvider', () => { , ); - expect(global.localStorage.setItem.calledWith(DEFAULT_MODE_STORAGE_KEY, 'system')).to.equal( - true, - ); - fireEvent.click(screen.getByRole('button', { name: 'change to dark' })); - expect(global.localStorage.setItem.calledWith(DEFAULT_MODE_STORAGE_KEY, 'dark')).to.equal( + expect(window.localStorage.setItem.calledWith(DEFAULT_MODE_STORAGE_KEY, 'dark')).to.equal( true, ); }); diff --git a/packages/mui-system/src/cssVars/index.ts b/packages/mui-system/src/cssVars/index.ts index e20a5bec491de3..63df8b222a9817 100644 --- a/packages/mui-system/src/cssVars/index.ts +++ b/packages/mui-system/src/cssVars/index.ts @@ -10,3 +10,4 @@ export { default as prepareTypographyVars } from './prepareTypographyVars'; export type { ExtractTypographyTokens } from './prepareTypographyVars'; export { default as createCssVarsTheme } from './createCssVarsTheme'; export { createGetColorSchemeSelector } from './getColorSchemeSelector'; +export type { StorageManager } from './localStorageManager'; diff --git a/packages/mui-system/src/cssVars/localStorageManager.ts b/packages/mui-system/src/cssVars/localStorageManager.ts new file mode 100644 index 00000000000000..c1e793c53179f8 --- /dev/null +++ b/packages/mui-system/src/cssVars/localStorageManager.ts @@ -0,0 +1,80 @@ +export interface StorageManager { + (options: { key: string; storageWindow?: Window | null }): { + /** + * Function to get the value from the storage + * @param defaultValue The default value to be returned if the key is not found + * @returns The value from the storage or the default value + */ + get(defaultValue: any): any; + /** + * Function to set the value in the storage + * @param value The value to be set + * @returns void + */ + set(value: any): void; + /** + * Function to subscribe to the value of the specified key triggered by external events + * @param handler The function to be called when the value changes + * @returns A function to unsubscribe the handler + * @example + * React.useEffect(() => { + * const unsubscribe = storageManager.subscribe((value) => { + * console.log(value); + * }); + * return unsubscribe; + * }, []); + */ + subscribe(handler: (value: any) => void): () => void; + }; +} + +function noop() {} + +const localStorageManager: StorageManager = ({ key, storageWindow }) => { + if (!storageWindow && typeof window !== 'undefined') { + storageWindow = window; + } + return { + get(defaultValue) { + if (typeof window === 'undefined') { + return undefined; + } + if (!storageWindow) { + return defaultValue; + } + let value; + try { + value = storageWindow.localStorage.getItem(key); + } catch { + // Unsupported + } + return value || defaultValue; + }, + set: (value) => { + if (storageWindow) { + try { + storageWindow.localStorage.setItem(key, value); + } catch { + // Unsupported + } + } + }, + subscribe: (handler) => { + if (!storageWindow) { + return noop; + } + const listener = (event: StorageEvent) => { + const value = event.newValue; + if (event.key === key) { + handler(value); + } + }; + storageWindow.addEventListener('storage', listener); + return () => { + storageWindow.removeEventListener('storage', listener); + }; + }, + }; +}; + +export default localStorageManager; diff --git a/packages/mui-system/src/cssVars/useCurrentColorScheme.test.js b/packages/mui-system/src/cssVars/useCurrentColorScheme.test.js index 7143167618502f..a6846765e0babb 100644 --- a/packages/mui-system/src/cssVars/useCurrentColorScheme.test.js +++ b/packages/mui-system/src/cssVars/useCurrentColorScheme.test.js @@ -13,7 +13,7 @@ describe('useCurrentColorScheme', () => { let originalMatchmedia; let originalAddEventListener; let storage = {}; - let storageHandler = {}; + const eventHandlers = new Map(); let trigger; const createMatchMedia = (matches) => () => ({ @@ -32,7 +32,18 @@ describe('useCurrentColorScheme', () => { before(() => { originalAddEventListener = window.addEventListener; window.addEventListener = (key, handler) => { - storageHandler[key] = handler; + if (eventHandlers.has(key)) { + eventHandlers.get(key).listeners.push(handler); + } else { + eventHandlers.set(key, { + listeners: [handler], + broadcastEvent(event) { + this.listeners.forEach((listener) => { + listener(event); + }); + }, + }); + } }; }); @@ -45,7 +56,7 @@ describe('useCurrentColorScheme', () => { // clear the localstorage storage = {}; // Create mocks of localStorage getItem and setItem functions - Object.defineProperty(global, 'localStorage', { + Object.defineProperty(window, 'localStorage', { value: { getItem: spy((key) => storage[key]), setItem: spy((key, value) => { @@ -55,7 +66,6 @@ describe('useCurrentColorScheme', () => { configurable: true, }); - storageHandler = {}; window.matchMedia = createMatchMedia(false); }); @@ -550,26 +560,10 @@ describe('useCurrentColorScheme', () => { fireEvent.click(container.firstChild); expect( - global.localStorage.setItem.lastCall.calledWith(DEFAULT_MODE_STORAGE_KEY, 'dark'), + window.localStorage.setItem.lastCall.calledWith(DEFAULT_MODE_STORAGE_KEY, 'dark'), ).to.equal(true); }); - it('save system mode', () => { - function Data() { - useCurrentColorScheme({ - defaultMode: 'system', - defaultLightColorScheme: 'light', - defaultDarkColorScheme: 'dark', - supportedColorSchemes: ['light', 'dark'], - }); - return null; - } - render(); - expect(global.localStorage.setItem.calledWith(DEFAULT_MODE_STORAGE_KEY, 'system')).to.equal( - true, - ); - }); - it('save lightColorScheme and darkColorScheme', () => { function Data() { const { setMode, setColorScheme, ...data } = useCurrentColorScheme({ @@ -593,11 +587,11 @@ describe('useCurrentColorScheme', () => { fireEvent.click(container.firstChild); - expect(global.localStorage.setItem.calledWith(DEFAULT_MODE_STORAGE_KEY, 'dark')).to.equal( + expect(window.localStorage.setItem.calledWith(DEFAULT_MODE_STORAGE_KEY, 'dark')).to.equal( true, ); expect( - global.localStorage.setItem.calledWith(`${DEFAULT_COLOR_SCHEME_STORAGE_KEY}-dark`, 'dim'), + window.localStorage.setItem.calledWith(`${DEFAULT_COLOR_SCHEME_STORAGE_KEY}-dark`, 'dim'), ).to.equal(true); }); @@ -654,7 +648,9 @@ describe('useCurrentColorScheme', () => { const { container } = render(); act(() => { - storageHandler.storage?.({ key: DEFAULT_MODE_STORAGE_KEY, newValue: 'dark' }); + eventHandlers + .get('storage') + .broadcastEvent?.({ key: DEFAULT_MODE_STORAGE_KEY, newValue: 'dark' }); }); expect(JSON.parse(container.firstChild.textContent)).to.deep.equal({ @@ -678,7 +674,9 @@ describe('useCurrentColorScheme', () => { const { container } = render(); act(() => { - storageHandler.storage?.({ key: DEFAULT_MODE_STORAGE_KEY, newValue: 'system' }); + eventHandlers + .get('storage') + .broadcastEvent?.({ key: DEFAULT_MODE_STORAGE_KEY, newValue: 'system' }); }); expect(JSON.parse(container.firstChild.textContent)).to.deep.equal({ @@ -704,7 +702,9 @@ describe('useCurrentColorScheme', () => { const { container } = render(); act(() => { - storageHandler.storage?.({ key: DEFAULT_MODE_STORAGE_KEY, newValue: null }); + eventHandlers + .get('storage') + .broadcastEvent?.({ key: DEFAULT_MODE_STORAGE_KEY, newValue: null }); }); expect(JSON.parse(container.firstChild.textContent)).to.deep.equal({ @@ -729,7 +729,7 @@ describe('useCurrentColorScheme', () => { const { container } = render(); act(() => { - storageHandler.storage?.({ + eventHandlers.get('storage').broadcastEvent?.({ key: `${DEFAULT_COLOR_SCHEME_STORAGE_KEY}-light`, newValue: 'light-dim', }); @@ -744,7 +744,7 @@ describe('useCurrentColorScheme', () => { }); act(() => { - storageHandler.storage?.({ + eventHandlers.get('storage').broadcastEvent?.({ key: `${DEFAULT_COLOR_SCHEME_STORAGE_KEY}-dark`, newValue: 'dark-dim', }); @@ -785,7 +785,7 @@ describe('useCurrentColorScheme', () => { fireEvent.click(screen.getByTestId('reset')); expect( - global.localStorage.setItem.lastCall.calledWith(DEFAULT_MODE_STORAGE_KEY, 'system'), + window.localStorage.setItem.lastCall.calledWith(DEFAULT_MODE_STORAGE_KEY, 'system'), ).to.equal(true); }); @@ -808,20 +808,133 @@ describe('useCurrentColorScheme', () => { fireEvent.click(screen.getByTestId('dark')); - global.localStorage.setItem.resetHistory(); - expect(global.localStorage.setItem.callCount).to.equal(0); // reset the calls to neglect initial setItem in the assertion below + window.localStorage.setItem.resetHistory(); + expect(window.localStorage.setItem.callCount).to.equal(0); // reset the calls to neglect initial setItem in the assertion below fireEvent.click(screen.getByTestId('reset')); expect( - global.localStorage.setItem.calledWith( + window.localStorage.setItem.calledWith( `${DEFAULT_COLOR_SCHEME_STORAGE_KEY}-light`, 'light', ), ).to.equal(true); expect( - global.localStorage.setItem.calledWith(`${DEFAULT_COLOR_SCHEME_STORAGE_KEY}-dark`, 'dark'), + window.localStorage.setItem.calledWith(`${DEFAULT_COLOR_SCHEME_STORAGE_KEY}-dark`, 'dark'), ).to.equal(true); }); }); + + describe('Custom storage', () => { + let cache = {}; + + beforeEach(() => { + cache = {}; + }); + + const storageManager = ({ key }) => ({ + get(defaultValue) { + return cache[key] || defaultValue; + }, + set(value) { + cache[key] = value; + }, + subscribe: (handler) => { + const listener = (event) => { + const value = event.newValue; + if (event.key === key) { + handler(value); + } + }; + window.addEventListener('storage', listener); + return () => { + window.removeEventListener('storage', listener); + }; + }, + }); + + it('use custom storage', () => { + function Data() { + const { setMode, ...data } = useCurrentColorScheme({ + storageManager, + defaultMode: 'light', + defaultLightColorScheme: 'light', + defaultDarkColorScheme: 'dark', + supportedColorSchemes: ['light', 'dark'], + }); + return ( + + ); + } + const { container } = render(); + + fireEvent.click(container.firstChild); + + expect(storageManager({ key: DEFAULT_MODE_STORAGE_KEY }).get()).to.equal('dark'); + }); + + it('handle subscription', () => { + function Data() { + const { setMode, ...data } = useCurrentColorScheme({ + storageManager, + defaultMode: 'light', + defaultLightColorScheme: 'light', + defaultDarkColorScheme: 'dark', + supportedColorSchemes: ['light', 'dark'], + }); + return ( + + ); + } + const { container } = render(); + + act(() => { + eventHandlers.get('storage').broadcastEvent?.({ + key: DEFAULT_MODE_STORAGE_KEY, + newValue: 'dark', + }); + }); + + expect(JSON.parse(container.firstChild.textContent)).to.deep.equal({ + mode: 'dark', + lightColorScheme: 'light', + darkColorScheme: 'dark', + colorScheme: 'dark', + }); + }); + + it('able to disable storage manager', () => { + function Data() { + const { setMode, ...data } = useCurrentColorScheme({ + storageManager: null, + defaultMode: 'light', + defaultLightColorScheme: 'light', + defaultDarkColorScheme: 'dark', + supportedColorSchemes: ['light', 'dark'], + }); + return ( + + ); + } + expect(() => render()).not.to.throw(); + }); + }); }); diff --git a/packages/mui-system/src/cssVars/useCurrentColorScheme.ts b/packages/mui-system/src/cssVars/useCurrentColorScheme.ts index f504172a6e2807..4a214cd092c43f 100644 --- a/packages/mui-system/src/cssVars/useCurrentColorScheme.ts +++ b/packages/mui-system/src/cssVars/useCurrentColorScheme.ts @@ -4,6 +4,8 @@ import { DEFAULT_MODE_STORAGE_KEY, DEFAULT_COLOR_SCHEME_STORAGE_KEY, } from '../InitColorSchemeScript/InitColorSchemeScript'; +import type { StorageManager } from './localStorageManager'; +import localStorageManager from './localStorageManager'; export type Mode = 'light' | 'dark' | 'system'; export type SystemMode = Exclude; @@ -53,6 +55,8 @@ export type Result = State void; }; +function noop() {} + export function getSystemMode(mode: undefined | string): SystemMode | undefined { if ( typeof window !== 'undefined' && @@ -95,23 +99,6 @@ export function getColorScheme( }); } -function initializeValue(key: string, defaultValue: string) { - if (typeof window === 'undefined') { - return undefined; - } - let value; - try { - value = localStorage.getItem(key) || undefined; - if (!value) { - // the first time that user enters the site. - localStorage.setItem(key, defaultValue); - } - } catch { - // Unsupported - } - return value || defaultValue; -} - interface UseCurrentColoSchemeOptions { defaultLightColorScheme: SupportedColorScheme; defaultDarkColorScheme: SupportedColorScheme; @@ -120,6 +107,7 @@ interface UseCurrentColoSchemeOptions { modeStorageKey?: string; colorSchemeStorageKey?: string; storageWindow?: Window | null; + storageManager?: StorageManager | null; noSsr?: boolean; } @@ -134,22 +122,29 @@ export default function useCurrentColorScheme 1; + const modeStorage = React.useMemo( + () => storageManager?.({ key: modeStorageKey, storageWindow }), + [storageManager, modeStorageKey, storageWindow], + ); + const lightStorage = React.useMemo( + () => storageManager?.({ key: `${colorSchemeStorageKey}-light`, storageWindow }), + [storageManager, colorSchemeStorageKey, storageWindow], + ); + const darkStorage = React.useMemo( + () => storageManager?.({ key: `${colorSchemeStorageKey}-dark`, storageWindow }), + [storageManager, colorSchemeStorageKey, storageWindow], + ); const [state, setState] = React.useState(() => { - const initialMode = initializeValue(modeStorageKey, defaultMode); - const lightColorScheme = initializeValue( - `${colorSchemeStorageKey}-light`, - defaultLightColorScheme, - ); - const darkColorScheme = initializeValue( - `${colorSchemeStorageKey}-dark`, - defaultDarkColorScheme, - ); + const initialMode = modeStorage?.get(defaultMode) || defaultMode; + const lightColorScheme = lightStorage?.get(defaultLightColorScheme) || defaultLightColorScheme; + const darkColorScheme = darkStorage?.get(defaultDarkColorScheme) || defaultDarkColorScheme; return { mode: initialMode, systemMode: getSystemMode(initialMode), @@ -172,11 +167,7 @@ export default function useCurrentColorScheme['setColorScheme'] = React.useCallback( (value) => { if (!value) { setState((currentState) => { - try { - localStorage.setItem(`${colorSchemeStorageKey}-light`, defaultLightColorScheme); - localStorage.setItem(`${colorSchemeStorageKey}-dark`, defaultDarkColorScheme); - } catch { - // Unsupported - } + lightStorage?.set(defaultLightColorScheme); + darkStorage?.set(defaultDarkColorScheme); return { ...currentState, lightColorScheme: defaultLightColorScheme, @@ -210,15 +197,12 @@ export default function useCurrentColorScheme { const newState = { ...currentState }; processState(currentState, (mode) => { - try { - localStorage.setItem(`${colorSchemeStorageKey}-${mode}`, value); - } catch { - // Unsupported - } if (mode === 'light') { + lightStorage?.set(value); newState.lightColorScheme = value; } if (mode === 'dark') { + darkStorage?.set(value); newState.darkColorScheme = value; } }); @@ -236,11 +220,7 @@ export default function useCurrentColorScheme { - if (storageWindow && isMultiSchemes) { - const handleStorage = (event: StorageEvent) => { - const value = event.newValue; - if ( - typeof event.key === 'string' && - event.key.startsWith(colorSchemeStorageKey) && - (!value || joinedColorSchemes.match(value)) - ) { - // If the key is deleted, value will be null then reset color scheme to the default one. - if (event.key.endsWith('light')) { + if (isMultiSchemes) { + const unsubscribeMode = + modeStorage?.subscribe((value: Mode) => { + if (!value || ['light', 'dark', 'system'].includes(value)) { + setMode((value as Mode) || defaultMode); + } + }) || noop; + const unsubscribeLight = + lightStorage?.subscribe((value: SupportedColorScheme) => { + if (!value || joinedColorSchemes.match(value)) { setColorScheme({ light: value as SupportedColorScheme | null }); } - if (event.key.endsWith('dark')) { + }) || noop; + const unsubscribeDark = + darkStorage?.subscribe((value: SupportedColorScheme) => { + if (!value || joinedColorSchemes.match(value)) { setColorScheme({ dark: value as SupportedColorScheme | null }); } - } - if ( - event.key === modeStorageKey && - (!value || ['light', 'dark', 'system'].includes(value)) - ) { - setMode((value as Mode) || defaultMode); - } - }; - // For syncing color-scheme changes between iframes - storageWindow.addEventListener('storage', handleStorage); + }) || noop; return () => { - storageWindow.removeEventListener('storage', handleStorage); + unsubscribeMode(); + unsubscribeLight(); + unsubscribeDark(); }; } return undefined; }, [ setColorScheme, setMode, - modeStorageKey, - colorSchemeStorageKey, joinedColorSchemes, defaultMode, storageWindow, isMultiSchemes, + modeStorage, + lightStorage, + darkStorage, ]); return {