diff --git a/docs/data/material/components/use-media-query/use-media-query.md b/docs/data/material/components/use-media-query/use-media-query.md index 6ff25eeb8c2c97..485214b0a4016b 100644 --- a/docs/data/material/components/use-media-query/use-media-query.md +++ b/docs/data/material/components/use-media-query/use-media-query.md @@ -94,9 +94,9 @@ describe('MyTests', () => { ## Client-side only rendering To perform the server-side hydration, the hook needs to render twice. -A first time with `false`, the value of the server, and a second time with the resolved value. -This double pass rendering cycle comes with a drawback. It's slower. -You can set the `noSsr` option to `true` if you are doing **client-side only** rendering. +A first time with `defaultMatches`, the value of the server, and a second time with the resolved value. +This double pass rendering cycle comes with a drawback: it's slower. +You can set the `noSsr` option to `true` if you use the returned value **only** client-side. ```js const matches = useMediaQuery('(min-width:600px)', { noSsr: true }); @@ -116,6 +116,10 @@ const theme = createTheme({ }); ``` +:::info +Note that `noSsr` has no effects when using the `createRoot()` API (the client side only API introduced in React 18). +::: + ## Server-side rendering :::warning @@ -201,14 +205,14 @@ You can reproduce the same behavior with a `useWidth` hook: - `options.defaultMatches` (_bool_ [optional]): As `window.matchMedia()` is unavailable on the server, - we return a default matches during the first mount. The default value is `false`. + it returns a default matches during the first mount. The default value is `false`. - `options.matchMedia` (_func_ [optional]): You can provide your own implementation of _matchMedia_. This can be used for handling an iframe content window. - `options.noSsr` (_bool_ [optional]): Defaults to `false`. To perform the server-side hydration, the hook needs to render twice. - A first time with `false`, the value of the server, and a second time with the resolved value. - This double pass rendering cycle comes with a drawback. It's slower. - You can set this option to `true` if you are doing **client-side only** rendering. -- `options.ssrMatchMedia` (_func_ [optional]): You can provide your own implementation of _matchMedia_ in a [server-side rendering context](#server-side-rendering). + A first time with `defaultMatches`, the value of the server, and a second time with the resolved value. + This double pass rendering cycle comes with a drawback: it's slower. + You can set this option to `true` if you use the returned value **only** client-side. +- `options.ssrMatchMedia` (_func_ [optional]): You can provide your own implementation of _matchMedia_, it's used when rendering server-side. Note: You can change the default options using the [`default props`](/material-ui/customization/theme-components/#default-props) feature of the theme with the `MuiUseMediaQuery` key. diff --git a/packages/mui-material/src/styles/props.d.ts b/packages/mui-material/src/styles/props.d.ts index 97ae5f767cf66f..f8f08561c9c7d3 100644 --- a/packages/mui-material/src/styles/props.d.ts +++ b/packages/mui-material/src/styles/props.d.ts @@ -69,7 +69,7 @@ import { MenuProps } from '../Menu'; import { MobileStepperProps } from '../MobileStepper'; import { ModalProps } from '../Modal'; import { NativeSelectProps } from '../NativeSelect'; -import { Options as useMediaQueryOptions } from '../useMediaQuery'; +import { UseMediaQueryOptions } from '../useMediaQuery'; import { OutlinedInputProps } from '../OutlinedInput'; import { PaginationProps } from '../Pagination'; import { PaginationItemProps } from '../PaginationItem'; @@ -241,5 +241,5 @@ export interface ComponentsPropsList { MuiTooltip: TooltipProps; MuiTouchRipple: TouchRippleProps; MuiTypography: TypographyProps; - MuiUseMediaQuery: useMediaQueryOptions; + MuiUseMediaQuery: UseMediaQueryOptions; } diff --git a/packages/mui-material/src/useMediaQuery/useMediaQuery.test.js b/packages/mui-material/src/useMediaQuery/useMediaQuery.test.js index fd14a2c4a052a1..d27351dab1ed25 100644 --- a/packages/mui-material/src/useMediaQuery/useMediaQuery.test.js +++ b/packages/mui-material/src/useMediaQuery/useMediaQuery.test.js @@ -144,7 +144,7 @@ describe('useMediaQuery', () => { expect(getRenderCountRef.current()).to.equal(1); }); - it('should render twice if the default value does not match the expectation', () => { + it('render API: should render once if the default value does not match the expectation', () => { const getRenderCountRef = React.createRef(); function Test() { const matches = useMediaQuery('(min-width:2000px)', { @@ -163,7 +163,7 @@ describe('useMediaQuery', () => { expect(getRenderCountRef.current()).to.equal(usesUseSyncExternalStore ? 1 : 2); }); - it('should render once if the default value does not match the expectation but `noSsr` is enabled', () => { + it('render API: should render once if the default value does not match the expectation but `noSsr` is enabled', () => { const getRenderCountRef = React.createRef(); function Test() { const matches = useMediaQuery('(min-width:2000px)', { @@ -182,6 +182,47 @@ describe('useMediaQuery', () => { expect(screen.getByTestId('matches').textContent).to.equal('false'); expect(getRenderCountRef.current()).to.equal(1); }); + + it('hydrate API: should render twice if the default value does not match the expectation', () => { + const getRenderCountRef = React.createRef(); + function Test() { + const matches = useMediaQuery('(min-width:2000px)', { + defaultMatches: true, + }); + + return ( + + {`${matches}`} + + ); + } + + const { hydrate } = renderToString(); + hydrate(); + expect(screen.getByTestId('matches').textContent).to.equal('false'); + expect(getRenderCountRef.current()).to.equal(2); + }); + + it('hydrate API: should render once if the default value does not match the expectation but `noSsr` is enabled', () => { + const getRenderCountRef = React.createRef(); + function Test() { + const matches = useMediaQuery('(min-width:2000px)', { + defaultMatches: true, + noSsr: true, + }); + + return ( + + {`${matches}`} + + ); + } + + const { hydrate } = renderToString(); + hydrate(); + expect(screen.getByTestId('matches').textContent).to.equal('false'); + expect(getRenderCountRef.current()).to.equal(1); + }); }); it('should try to reconcile each time', () => { diff --git a/packages/mui-material/src/useMediaQuery/useMediaQuery.ts b/packages/mui-material/src/useMediaQuery/useMediaQuery.ts index 328489e60f9be3..61214c6d1556ba 100644 --- a/packages/mui-material/src/useMediaQuery/useMediaQuery.ts +++ b/packages/mui-material/src/useMediaQuery/useMediaQuery.ts @@ -23,15 +23,29 @@ export interface MuiMediaQueryList { */ export type MuiMediaQueryListListener = (event: MuiMediaQueryListEvent) => void; -export interface Options { +export interface UseMediaQueryOptions { + /** + * As `window.matchMedia()` is unavailable on the server, + * it returns a default matches during the first mount. + * @default false + */ defaultMatches?: boolean; + /** + * You can provide your own implementation of matchMedia. + * This can be used for handling an iframe content window. + */ matchMedia?: typeof window.matchMedia; /** - * This option is kept for backwards compatibility and has no longer any effect. - * It's previous behavior is now handled automatically. + * To perform the server-side hydration, the hook needs to render twice. + * A first time with `defaultMatches`, the value of the server, and a second time with the resolved value. + * This double pass rendering cycle comes with a drawback: it's slower. + * You can set this option to `true` if you use the returned value **only** client-side. + * @default false */ - // TODO: Deprecate for v6 noSsr?: boolean; + /** + * You can provide your own implementation of `matchMedia`, it's used when rendering server-side. + */ ssrMatchMedia?: (query: string) => { matches: boolean }; } @@ -40,13 +54,10 @@ function useMediaQueryOld( defaultMatches: boolean, matchMedia: typeof window.matchMedia | null, ssrMatchMedia: ((query: string) => { matches: boolean }) | null, - noSsr: boolean | undefined, + noSsr: boolean, ): boolean { - const supportMatchMedia = - typeof window !== 'undefined' && typeof window.matchMedia !== 'undefined'; - const [match, setMatch] = React.useState(() => { - if (noSsr && supportMatchMedia) { + if (noSsr && matchMedia) { return matchMedia!(query).matches; } if (ssrMatchMedia) { @@ -61,7 +72,7 @@ function useMediaQueryOld( useEnhancedEffect(() => { let active = true; - if (!supportMatchMedia) { + if (!matchMedia) { return undefined; } @@ -81,7 +92,7 @@ function useMediaQueryOld( active = false; queryList.removeListener(updateMatch); }; - }, [query, matchMedia, supportMatchMedia]); + }, [query, matchMedia]); return match; } @@ -94,15 +105,20 @@ function useMediaQueryNew( defaultMatches: boolean, matchMedia: typeof window.matchMedia | null, ssrMatchMedia: ((query: string) => { matches: boolean }) | null, + noSsr: boolean, ): boolean { const getDefaultSnapshot = React.useCallback(() => defaultMatches, [defaultMatches]); const getServerSnapshot = React.useMemo(() => { + if (noSsr && matchMedia) { + return () => matchMedia!(query).matches; + } + if (ssrMatchMedia !== null) { const { matches } = ssrMatchMedia(query); return () => matches; } return getDefaultSnapshot; - }, [getDefaultSnapshot, query, ssrMatchMedia]); + }, [getDefaultSnapshot, query, ssrMatchMedia, noSsr, matchMedia]); const [getSnapshot, subscribe] = React.useMemo(() => { if (matchMedia === null) { return [getDefaultSnapshot, () => () => {}]; @@ -128,7 +144,7 @@ function useMediaQueryNew( export default function useMediaQuery( queryInput: string | ((theme: Theme) => string), - options: Options = {}, + options: UseMediaQueryOptions = {}, ): boolean { const theme = useTheme(); // Wait for jsdom to support the match media feature. @@ -141,7 +157,7 @@ export default function useMediaQuery( defaultMatches = false, matchMedia = supportMatchMedia ? window.matchMedia : null, ssrMatchMedia = null, - noSsr, + noSsr = false, } = getThemeProps({ name: 'MuiUseMediaQuery', props: options, theme }); if (process.env.NODE_ENV !== 'production') {