Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[system] Add disableClientRerender to prevent extra rerendering #44451

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 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,19 @@
</ThemeProvider>
```

## Disable rerender

By default, the `ThemeProvider` rerenders when the theme contains light **and** dark color schemes to prevent SSR hydration mismatches. To disable this behavior, apply the `disableRerender` prop as `true` to the `ThemeProvider` component:

```jsx
<ThemeProvider theme={theme} disableRerender>
```

This prop 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.

Check warning on line 145 in docs/data/material/customization/dark-mode/dark-mode.md

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Google.Will] Avoid using 'will'. Raw Output: {"message": "[Google.Will] Avoid using 'will'.", "location": {"path": "docs/data/material/customization/dark-mode/dark-mode.md", "range": {"start": {"line": 145, "column": 81}}}, "severity": "WARNING"}
- 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` will come 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
*/
disableClientRerender?: 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',
disableClientRerender,
} = 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,
disableClientRerender,
});

let mode = stateMode;
Expand Down Expand Up @@ -320,6 +322,11 @@ export default function createCssVarsProvider(options) {
* require the theme to have `colorSchemes` with light and dark.
*/
defaultMode: 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.
*/
disableClientRerender: PropTypes.bool,
/**
* If `true`, the provider creates its own context and generate stylesheet as if it is a root `CssVarsProvider`.
*/
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('[disableClientRerender] does not trigger a re-render', () => {
function Data() {
const { mode } = useCurrentColorScheme({
defaultMode: 'dark',
supportedColorSchemes: ['light', 'dark'],
disableClientRerender: 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;
disableClientRerender?: 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,
disableClientRerender = 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(disableClientRerender || !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
Loading
Loading