Skip to content

Commit 50a760e

Browse files
[material-ui] Add storageManager prop to ThemeProvider (@siriwatknp) (#45437)
1 parent 883b844 commit 50a760e

12 files changed

+401
-121
lines changed

docs/data/material/customization/dark-mode/dark-mode.md

+72
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,78 @@ The `mode` is always `undefined` on first render, so make sure to handle this ca
122122

123123
{{"demo": "ToggleColorMode.js", "defaultCodeOpen": false}}
124124

125+
## Storage manager
126+
127+
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.
128+
129+
To use a different storage manager, create a custom function with this signature:
130+
131+
```ts
132+
type Unsubscribe = () => void;
133+
134+
function storageManager(params: { key: string }): {
135+
get: (defaultValue: any) => any;
136+
set: (value: any) => void;
137+
subscribe: (handler: (value: any) => void) => Unsubscribe;
138+
};
139+
```
140+
141+
Then pass it to the `storageManager` prop of the `ThemeProvider` component:
142+
143+
```tsx
144+
import { ThemeProvider, createTheme } from '@mui/material/styles';
145+
import type { StorageManager } from '@mui/material/styles';
146+
147+
const theme = createTheme({
148+
colorSchemes: {
149+
dark: true,
150+
},
151+
});
152+
153+
function storageManager(params): StorageManager {
154+
return {
155+
get: (defaultValue) => {
156+
// Your implementation
157+
},
158+
set: (value) => {
159+
// Your implementation
160+
},
161+
subscribe: (handler) => {
162+
// Your implementation
163+
return () => {
164+
// cleanup
165+
};
166+
},
167+
};
168+
}
169+
170+
function App() {
171+
return (
172+
<ThemeProvider theme={theme} storageManager={storageManager}>
173+
...
174+
</ThemeProvider>
175+
);
176+
}
177+
```
178+
179+
:::warning
180+
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.
181+
:::
182+
183+
### Disable storage
184+
185+
To disable the storage manager, pass `null` to the `storageManager` prop:
186+
187+
```tsx
188+
<ThemeProvider theme={theme} storageManager={null}>
189+
...
190+
</ThemeProvider>
191+
```
192+
193+
:::warning
194+
Disabling the storage manager will cause the app to reset to its default mode whenever the user refreshes the page.
195+
:::
196+
125197
## Disable transitions
126198

127199
To instantly switch between color schemes with no transition, apply the `disableTransitionOnChange` prop to the `ThemeProvider` component:

packages/mui-material/src/styles/ThemeProvider.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('ThemeProvider', () => {
1212
originalMatchmedia = window.matchMedia;
1313
// Create mocks of localStorage getItem and setItem functions
1414
storage = {};
15-
Object.defineProperty(global, 'localStorage', {
15+
Object.defineProperty(window, 'localStorage', {
1616
value: {
1717
getItem: (key: string) => storage[key],
1818
setItem: (key: string, value: string) => {

packages/mui-material/src/styles/ThemeProvider.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22
import * as React from 'react';
33
import { DefaultTheme } from '@mui/system';
4+
import { StorageManager } from '@mui/system/cssVars';
45
import ThemeProviderNoVars from './ThemeProviderNoVars';
56
import { CssThemeVariables } from './createThemeNoVars';
67
import { CssVarsProvider } from './ThemeProviderWithVars';
@@ -47,6 +48,11 @@ export interface ThemeProviderProps<Theme = DefaultTheme> extends ThemeProviderC
4748
* @default window
4849
*/
4950
storageWindow?: Window | null;
51+
/**
52+
* The storage manager to be used for storing the mode and color scheme
53+
* @default using `window.localStorage`
54+
*/
55+
storageManager?: StorageManager | null;
5056
/**
5157
* localStorage key used to store application `mode`
5258
* @default 'mui-mode'

packages/mui-material/src/styles/ThemeProviderWithVars.spec.tsx

+22-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import * as React from 'react';
2-
import { extendTheme, CssVarsProvider, styled, useTheme, Overlays } from '@mui/material/styles';
2+
import {
3+
extendTheme,
4+
ThemeProvider,
5+
styled,
6+
useTheme,
7+
Overlays,
8+
StorageManager,
9+
} from '@mui/material/styles';
310
import type {} from '@mui/material/themeCssVarsAugmentation';
411

512
const customTheme = extendTheme({
@@ -53,7 +60,7 @@ function TestUseTheme() {
5360
return <div style={{ background: theme.vars.palette.common.background }}>test</div>;
5461
}
5562

56-
<CssVarsProvider theme={customTheme}>
63+
<ThemeProvider theme={customTheme}>
5764
<TestStyled
5865
sx={(theme) => ({
5966
// test that `theme` in sx has access to CSS vars
@@ -63,4 +70,16 @@ function TestUseTheme() {
6370
},
6471
})}
6572
/>
66-
</CssVarsProvider>;
73+
</ThemeProvider>;
74+
75+
<ThemeProvider theme={customTheme} storageManager={null} />;
76+
77+
const storageManager: StorageManager = () => {
78+
return {
79+
get: () => 'light',
80+
set: () => {},
81+
subscribe: () => () => {},
82+
};
83+
};
84+
85+
<ThemeProvider theme={customTheme} storageManager={storageManager} />;

packages/mui-material/src/styles/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export { default as withStyles } from './withStyles';
103103
export { default as withTheme } from './withTheme';
104104

105105
export * from './ThemeProviderWithVars';
106+
export type { StorageManager } from '@mui/system/cssVars';
106107

107108
export { default as extendTheme } from './createThemeWithVars';
108109

packages/mui-system/src/cssVars/createCssVarsProvider.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react';
22
import InitColorSchemeScript from '../InitColorSchemeScript';
33
import { Result } from './useCurrentColorScheme';
4+
import type { StorageManager } from './localStorageManager';
45

56
export interface ColorSchemeContextValue<SupportedColorScheme extends string>
67
extends Result<SupportedColorScheme> {
@@ -70,6 +71,11 @@ export interface CreateCssVarsProviderResult<
7071
* @default document
7172
*/
7273
colorSchemeNode?: Element | null;
74+
/**
75+
* The storage manager to be used for storing the mode and color scheme.
76+
* @default using `window.localStorage`
77+
*/
78+
storageManager?: StorageManager | null;
7379
/**
7480
* The window that attaches the 'storage' event listener
7581
* @default window

packages/mui-system/src/cssVars/createCssVarsProvider.js

+7
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export default function createCssVarsProvider(options) {
6060
modeStorageKey = defaultModeStorageKey,
6161
colorSchemeStorageKey = defaultColorSchemeStorageKey,
6262
disableTransitionOnChange = designSystemTransitionOnChange,
63+
storageManager,
6364
storageWindow = typeof window === 'undefined' ? undefined : window,
6465
documentNode = typeof document === 'undefined' ? undefined : document,
6566
colorSchemeNode = typeof document === 'undefined' ? undefined : document.documentElement,
@@ -119,6 +120,7 @@ export default function createCssVarsProvider(options) {
119120
modeStorageKey,
120121
colorSchemeStorageKey,
121122
defaultMode,
123+
storageManager,
122124
storageWindow,
123125
noSsr,
124126
});
@@ -357,6 +359,11 @@ export default function createCssVarsProvider(options) {
357359
* You should use this option in conjuction with `InitColorSchemeScript` component.
358360
*/
359361
noSsr: PropTypes.bool,
362+
/**
363+
* The storage manager to be used for storing the mode and color scheme
364+
* @default using `window.localStorage`
365+
*/
366+
storageManager: PropTypes.func,
360367
/**
361368
* The window that attaches the 'storage' event listener.
362369
* @default window

packages/mui-system/src/cssVars/createCssVarsProvider.test.js

+2-6
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('createCssVarsProvider', () => {
2828
originalMatchmedia = window.matchMedia;
2929

3030
// Create mocks of localStorage getItem and setItem functions
31-
Object.defineProperty(global, 'localStorage', {
31+
Object.defineProperty(window, 'localStorage', {
3232
value: {
3333
getItem: spy((key) => storage[key]),
3434
setItem: spy((key, value) => {
@@ -584,13 +584,9 @@ describe('createCssVarsProvider', () => {
584584
</CssVarsProvider>,
585585
);
586586

587-
expect(global.localStorage.setItem.calledWith(DEFAULT_MODE_STORAGE_KEY, 'system')).to.equal(
588-
true,
589-
);
590-
591587
fireEvent.click(screen.getByRole('button', { name: 'change to dark' }));
592588

593-
expect(global.localStorage.setItem.calledWith(DEFAULT_MODE_STORAGE_KEY, 'dark')).to.equal(
589+
expect(window.localStorage.setItem.calledWith(DEFAULT_MODE_STORAGE_KEY, 'dark')).to.equal(
594590
true,
595591
);
596592
});

packages/mui-system/src/cssVars/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export { default as prepareTypographyVars } from './prepareTypographyVars';
1010
export type { ExtractTypographyTokens } from './prepareTypographyVars';
1111
export { default as createCssVarsTheme } from './createCssVarsTheme';
1212
export { createGetColorSchemeSelector } from './getColorSchemeSelector';
13+
export type { StorageManager } from './localStorageManager';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
export interface StorageManager {
2+
(options: { key: string; storageWindow?: Window | null }): {
3+
/**
4+
* Function to get the value from the storage
5+
* @param defaultValue The default value to be returned if the key is not found
6+
* @returns The value from the storage or the default value
7+
*/
8+
get(defaultValue: any): any;
9+
/**
10+
* Function to set the value in the storage
11+
* @param value The value to be set
12+
* @returns void
13+
*/
14+
set(value: any): void;
15+
/**
16+
* Function to subscribe to the value of the specified key triggered by external events
17+
* @param handler The function to be called when the value changes
18+
* @returns A function to unsubscribe the handler
19+
* @example
20+
* React.useEffect(() => {
21+
* const unsubscribe = storageManager.subscribe((value) => {
22+
* console.log(value);
23+
* });
24+
* return unsubscribe;
25+
* }, []);
26+
*/
27+
subscribe(handler: (value: any) => void): () => void;
28+
};
29+
}
30+
31+
function noop() {}
32+
33+
const localStorageManager: StorageManager = ({ key, storageWindow }) => {
34+
if (!storageWindow && typeof window !== 'undefined') {
35+
storageWindow = window;
36+
}
37+
return {
38+
get(defaultValue) {
39+
if (typeof window === 'undefined') {
40+
return undefined;
41+
}
42+
if (!storageWindow) {
43+
return defaultValue;
44+
}
45+
let value;
46+
try {
47+
value = storageWindow.localStorage.getItem(key);
48+
} catch {
49+
// Unsupported
50+
}
51+
return value || defaultValue;
52+
},
53+
set: (value) => {
54+
if (storageWindow) {
55+
try {
56+
storageWindow.localStorage.setItem(key, value);
57+
} catch {
58+
// Unsupported
59+
}
60+
}
61+
},
62+
subscribe: (handler) => {
63+
if (!storageWindow) {
64+
return noop;
65+
}
66+
const listener = (event: StorageEvent) => {
67+
const value = event.newValue;
68+
if (event.key === key) {
69+
handler(value);
70+
}
71+
};
72+
storageWindow.addEventListener('storage', listener);
73+
return () => {
74+
storageWindow.removeEventListener('storage', listener);
75+
};
76+
},
77+
};
78+
};
79+
80+
export default localStorageManager;

0 commit comments

Comments
 (0)