Skip to content

Commit

Permalink
[system] Add ThemeProvider noSsr to prevent double rendering (#44451)
Browse files Browse the repository at this point in the history
  • Loading branch information
siriwatknp authored Nov 26, 2024
1 parent 0c51224 commit 72823b8
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 11 deletions.
15 changes: 15 additions & 0 deletions docs/data/material/customization/dark-mode/dark-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,21 @@ To instantly switch between color schemes with no transition, apply the `disable
</ThemeProvider>
```

## 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
<ThemeProvider theme={theme} noSsr>
```

`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.
Expand Down
6 changes: 6 additions & 0 deletions packages/mui-material/src/styles/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ export interface ThemeProviderProps<Theme = DefaultTheme> 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
Expand Down
7 changes: 7 additions & 0 deletions packages/mui-system/src/cssVars/createCssVarsProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -114,6 +115,7 @@ export default function createCssVarsProvider(options) {
colorSchemeStorageKey,
defaultMode,
storageWindow,
noSsr,
});

let mode = stateMode;
Expand Down Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions packages/mui-system/src/cssVars/useCurrentColorScheme.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
{mode}:{count.current}
</div>
);
}
const { container } = render(<Data />);

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 (
<div>
{mode}:{count.current}
</div>
);
}
const { container } = render(<Data />);

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');
Expand Down
19 changes: 8 additions & 11 deletions packages/mui-system/src/cssVars/useCurrentColorScheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ interface UseCurrentColoSchemeOptions<SupportedColorScheme extends string> {
modeStorageKey?: string;
colorSchemeStorageKey?: string;
storageWindow?: Window | null;
noSsr?: boolean;
}

export default function useCurrentColorScheme<SupportedColorScheme extends string>(
Expand All @@ -133,6 +134,7 @@ export default function useCurrentColorScheme<SupportedColorScheme extends strin
modeStorageKey = DEFAULT_MODE_STORAGE_KEY,
colorSchemeStorageKey = DEFAULT_COLOR_SCHEME_STORAGE_KEY,
storageWindow = typeof window === 'undefined' ? undefined : window,
noSsr = false,
} = options;

const joinedColorSchemes = supportedColorSchemes.join(',');
Expand All @@ -155,15 +157,10 @@ export default function useCurrentColorScheme<SupportedColorScheme extends strin
darkColorScheme,
} as State<SupportedColorScheme>;
});
// 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);

Expand Down Expand Up @@ -350,9 +347,9 @@ export default function useCurrentColorScheme<SupportedColorScheme extends strin

return {
...state,
mode: hasMounted.current || !isMultiSchemes ? state.mode : undefined,
systemMode: hasMounted.current || !isMultiSchemes ? state.systemMode : undefined,
colorScheme: hasMounted.current || !isMultiSchemes ? colorScheme : undefined,
mode: isClient ? state.mode : undefined,
systemMode: isClient ? state.systemMode : undefined,
colorScheme: isClient ? colorScheme : undefined,
setMode,
setColorScheme,
};
Expand Down

0 comments on commit 72823b8

Please sign in to comment.