From d0a59894122d209958beda64e17f5987fef141b2 Mon Sep 17 00:00:00 2001 From: Siriwat K Date: Fri, 29 Nov 2024 13:45:11 +0700 Subject: [PATCH] [ThemeProvider] Optimize `theme` changes when enabling CSS theme variables (#44588) --- .../src/styles/ThemeProviderWithVars.test.js | 31 +++++++ .../src/cssVars/createCssVarsProvider.js | 92 ++++++++++--------- 2 files changed, 80 insertions(+), 43 deletions(-) diff --git a/packages/mui-material/src/styles/ThemeProviderWithVars.test.js b/packages/mui-material/src/styles/ThemeProviderWithVars.test.js index 57cc558313c691..e06a6d304d63dd 100644 --- a/packages/mui-material/src/styles/ThemeProviderWithVars.test.js +++ b/packages/mui-material/src/styles/ThemeProviderWithVars.test.js @@ -408,4 +408,35 @@ describe('[Material UI] ThemeProviderWithVars', () => { fireEvent.click(screen.getByText('Dark')); }).not.toErrorDev(); }); + + it('theme should remain the same when ThemeProvider rerenders', () => { + const theme = createTheme({ cssVariables: true }); + + function Inner() { + const upperTheme = useTheme(); + const themeRef = React.useRef(upperTheme); + const [changed, setChanged] = React.useState(false); + React.useEffect(() => { + if (themeRef.current !== upperTheme) { + setChanged(true); + } + }, [upperTheme]); + return changed ?
: null; + } + function App() { + const [, setState] = React.useState({}); + const rerender = () => setState({}); + return ( + + + + + ); + } + render(); + + fireEvent.click(screen.getByRole('button')); + + expect(screen.queryByTestId('theme-changed')).to.equal(null); + }); }); diff --git a/packages/mui-system/src/cssVars/createCssVarsProvider.js b/packages/mui-system/src/cssVars/createCssVarsProvider.js index e7a2a21febb207..64ed9f9fc4037d 100644 --- a/packages/mui-system/src/cssVars/createCssVarsProvider.js +++ b/packages/mui-system/src/cssVars/createCssVarsProvider.js @@ -48,6 +48,9 @@ export default function createCssVarsProvider(options) { const useColorScheme = () => React.useContext(ColorSchemeContext) || defaultContext; + const defaultColorSchemes = {}; + const defaultComponents = {}; + function CssVarsProvider(props) { const { children, @@ -75,12 +78,12 @@ export default function createCssVarsProvider(options) { return typeof defaultTheme === 'function' ? defaultTheme() : defaultTheme; }, [themeProp]); const scopedTheme = initialTheme[themeId]; + const restThemeProp = scopedTheme || initialTheme; const { - colorSchemes = {}, - components = {}, + colorSchemes = defaultColorSchemes, + components = defaultComponents, cssVarPrefix, - ...restThemeProp - } = scopedTheme || initialTheme; + } = restThemeProp; const joinedColorSchemes = Object.keys(colorSchemes) .filter((k) => !!colorSchemes[k]) .join(','); @@ -126,42 +129,46 @@ export default function createCssVarsProvider(options) { colorScheme = ctx.colorScheme; } - // `colorScheme` is undefined on the server and hydration phase - const calculatedColorScheme = colorScheme || restThemeProp.defaultColorScheme; + const memoTheme = React.useMemo(() => { + // `colorScheme` is undefined on the server and hydration phase + const calculatedColorScheme = colorScheme || restThemeProp.defaultColorScheme; - // 2. get the `vars` object that refers to the CSS custom properties - const themeVars = restThemeProp.generateThemeVars?.() || restThemeProp.vars; + // 2. get the `vars` object that refers to the CSS custom properties + const themeVars = restThemeProp.generateThemeVars?.() || restThemeProp.vars; - // 3. Start composing the theme object - const theme = { - ...restThemeProp, - components, - colorSchemes, - cssVarPrefix, - vars: themeVars, - }; - if (typeof theme.generateSpacing === 'function') { - theme.spacing = theme.generateSpacing(); - } + // 3. Start composing the theme object + const theme = { + ...restThemeProp, + components, + colorSchemes, + cssVarPrefix, + vars: themeVars, + }; + if (typeof theme.generateSpacing === 'function') { + theme.spacing = theme.generateSpacing(); + } - // 4. Resolve the color scheme and merge it to the theme - if (calculatedColorScheme) { - const scheme = colorSchemes[calculatedColorScheme]; - if (scheme && typeof scheme === 'object') { - // 4.1 Merge the selected color scheme to the theme - Object.keys(scheme).forEach((schemeKey) => { - if (scheme[schemeKey] && typeof scheme[schemeKey] === 'object') { - // shallow merge the 1st level structure of the theme. - theme[schemeKey] = { - ...theme[schemeKey], - ...scheme[schemeKey], - }; - } else { - theme[schemeKey] = scheme[schemeKey]; - } - }); + // 4. Resolve the color scheme and merge it to the theme + if (calculatedColorScheme) { + const scheme = colorSchemes[calculatedColorScheme]; + if (scheme && typeof scheme === 'object') { + // 4.1 Merge the selected color scheme to the theme + Object.keys(scheme).forEach((schemeKey) => { + if (scheme[schemeKey] && typeof scheme[schemeKey] === 'object') { + // shallow merge the 1st level structure of the theme. + theme[schemeKey] = { + ...theme[schemeKey], + ...scheme[schemeKey], + }; + } else { + theme[schemeKey] = scheme[schemeKey]; + } + }); + } } - } + + return resolveTheme ? resolveTheme(theme) : theme; + }, [restThemeProp, colorScheme, components, colorSchemes, cssVarPrefix]); // 5. Declaring effects // 5.1 Updates the selector value to use the current color scheme which tells CSS to use the proper stylesheet. @@ -248,7 +255,7 @@ export default function createCssVarsProvider(options) { process.env.NODE_ENV === 'production' ? setMode : (newMode) => { - if (theme.colorSchemeSelector === 'media') { + if (memoTheme.colorSchemeSelector === 'media') { console.error( [ 'MUI: The `setMode` function has no effect if `colorSchemeSelector` is `media` (`media` is the default value).', @@ -270,7 +277,7 @@ export default function createCssVarsProvider(options) { setColorScheme, setMode, systemMode, - theme.colorSchemeSelector, + memoTheme.colorSchemeSelector, ], ); @@ -285,13 +292,12 @@ export default function createCssVarsProvider(options) { const element = ( - + {children} - {shouldGenerateStyleSheet && } + {shouldGenerateStyleSheet && ( + + )} );