diff --git a/docs/data/material/customization/dark-mode/dark-mode.md b/docs/data/material/customization/dark-mode/dark-mode.md index 1ac10aa18cc0ea..77c76d8d820b07 100644 --- a/docs/data/material/customization/dark-mode/dark-mode.md +++ b/docs/data/material/customization/dark-mode/dark-mode.md @@ -132,6 +132,21 @@ To instantly switch between color schemes with no transition, apply the `disable ``` +## Disable double rendering + +By default, the `ThemeProvider` rerenders when the theme contains light **and** dark color schemes to prevent SSR hydration mismatches. + +To disable this behavior, use the `noSsr` prop: + +```jsx + +``` + +`noSsr` is useful if you are building: + +- A client-only application, such as a single-page application (SPA). This prop will optimize the performance and prevent the dark mode flickering when users refresh the page. +- A server-rendered application with [Suspense](https://react.dev/reference/react/Suspense). However, you must ensure that the server render output matches the initial render output on the client. + ## Setting the default mode When `colorSchemes` is provided, the default mode is `system`, which means the app uses the system preference when users first visit the site. diff --git a/packages/mui-material/src/styles/ThemeProvider.tsx b/packages/mui-material/src/styles/ThemeProvider.tsx index ffdab139176ea9..e9e0dc5fb94c82 100644 --- a/packages/mui-material/src/styles/ThemeProvider.tsx +++ b/packages/mui-material/src/styles/ThemeProvider.tsx @@ -57,6 +57,12 @@ export interface ThemeProviderProps extends ThemeProviderC * @default 'mui-color-scheme' */ colorSchemeStorageKey?: string; + /* + * If `true`, ThemeProvider will not rerender and the initial value of `mode` comes from the local storage. + * For SSR applications, you must ensure that the server render output must match the initial render output on the client. + * @default false + */ + noSsr?: boolean; /** * Disable CSS transitions when switching between modes or color schemes * @default false diff --git a/packages/mui-system/src/cssVars/createCssVarsProvider.js b/packages/mui-system/src/cssVars/createCssVarsProvider.js index cfed67e92e4f33..e7a2a21febb207 100644 --- a/packages/mui-system/src/cssVars/createCssVarsProvider.js +++ b/packages/mui-system/src/cssVars/createCssVarsProvider.js @@ -61,6 +61,7 @@ export default function createCssVarsProvider(options) { disableNestedContext = false, disableStyleSheetGeneration = false, defaultMode: initialMode = 'system', + noSsr, } = props; const hasMounted = React.useRef(false); const upperTheme = muiUseTheme(); @@ -114,6 +115,7 @@ export default function createCssVarsProvider(options) { colorSchemeStorageKey, defaultMode, storageWindow, + noSsr, }); let mode = stateMode; @@ -342,6 +344,11 @@ export default function createCssVarsProvider(options) { * The key in the local storage used to store current color scheme. */ modeStorageKey: PropTypes.string, + /** + * If `true`, the mode will be the same value as the storage without an extra rerendering after the hydration. + * You should use this option in conjuction with `InitColorSchemeScript` component. + */ + noSsr: PropTypes.bool, /** * The window that attaches the 'storage' event listener. * @default window diff --git a/packages/mui-system/src/cssVars/useCurrentColorScheme.test.js b/packages/mui-system/src/cssVars/useCurrentColorScheme.test.js index a0d75139ea55e6..6d25274aa6ff3e 100644 --- a/packages/mui-system/src/cssVars/useCurrentColorScheme.test.js +++ b/packages/mui-system/src/cssVars/useCurrentColorScheme.test.js @@ -84,6 +84,50 @@ describe('useCurrentColorScheme', () => { expect(container.firstChild.textContent).to.equal('dark:0'); }); + it('trigger a re-render for a multi color schemes', () => { + function Data() { + const { mode } = useCurrentColorScheme({ + supportedColorSchemes: ['light', 'dark'], + defaultLightColorScheme: 'light', + defaultDarkColorScheme: 'dark', + }); + const count = React.useRef(0); + React.useEffect(() => { + count.current += 1; + }); + return ( +
+ {mode}:{count.current} +
+ ); + } + const { container } = render(); + + expect(container.firstChild.textContent).to.equal('light:2'); // 2 because of double render within strict mode + }); + + it('[noSsr] does not trigger a re-render', () => { + function Data() { + const { mode } = useCurrentColorScheme({ + defaultMode: 'dark', + supportedColorSchemes: ['light', 'dark'], + noSsr: true, + }); + const count = React.useRef(0); + React.useEffect(() => { + count.current += 1; + }); + return ( +
+ {mode}:{count.current} +
+ ); + } + const { container } = render(); + + expect(container.firstChild.textContent).to.equal('dark:0'); + }); + describe('getColorScheme', () => { it('use lightColorScheme given mode=light', () => { expect(getColorScheme({ mode: 'light', lightColorScheme: 'light' })).to.equal('light'); diff --git a/packages/mui-system/src/cssVars/useCurrentColorScheme.ts b/packages/mui-system/src/cssVars/useCurrentColorScheme.ts index a318b1119d6a3b..f504172a6e2807 100644 --- a/packages/mui-system/src/cssVars/useCurrentColorScheme.ts +++ b/packages/mui-system/src/cssVars/useCurrentColorScheme.ts @@ -120,6 +120,7 @@ interface UseCurrentColoSchemeOptions { modeStorageKey?: string; colorSchemeStorageKey?: string; storageWindow?: Window | null; + noSsr?: boolean; } export default function useCurrentColorScheme( @@ -133,6 +134,7 @@ export default function useCurrentColorScheme; }); - // This could be improved with `React.useSyncExternalStore` in the future. - const [, setHasMounted] = React.useState(false); - const hasMounted = React.useRef(false); + const [isClient, setIsClient] = React.useState(noSsr || !isMultiSchemes); React.useEffect(() => { - if (isMultiSchemes) { - setHasMounted(true); // to rerender the component after hydration - } - hasMounted.current = true; - }, [isMultiSchemes]); + setIsClient(true); // to rerender the component after hydration + }, []); const colorScheme = getColorScheme(state); @@ -350,9 +347,9 @@ export default function useCurrentColorScheme