From 39a2df46c26be390cd8d65200ab347e12b217f9e Mon Sep 17 00:00:00 2001 From: Bram van der Holst Date: Wed, 6 Nov 2024 17:21:54 +0100 Subject: [PATCH] Added MediaQuery component to conditionally render (and hydrate) components based on a media query given --- .changeset/empty-grapes-decide.md | 5 ++ packages/next-ui/MediaQuery/MediaQuery.tsx | 87 ++++++++++++++++++++++ packages/next-ui/MediaQuery/index.ts | 1 + packages/next-ui/index.ts | 1 + 4 files changed, 94 insertions(+) create mode 100644 .changeset/empty-grapes-decide.md create mode 100644 packages/next-ui/MediaQuery/MediaQuery.tsx create mode 100644 packages/next-ui/MediaQuery/index.ts diff --git a/.changeset/empty-grapes-decide.md b/.changeset/empty-grapes-decide.md new file mode 100644 index 0000000000..571334a407 --- /dev/null +++ b/.changeset/empty-grapes-decide.md @@ -0,0 +1,5 @@ +--- +'@graphcommerce/next-ui': patch +--- + +Added MediaQuery component to conditionally render (and hydrate) components based on a media query given diff --git a/packages/next-ui/MediaQuery/MediaQuery.tsx b/packages/next-ui/MediaQuery/MediaQuery.tsx new file mode 100644 index 0000000000..6e9b8b914f --- /dev/null +++ b/packages/next-ui/MediaQuery/MediaQuery.tsx @@ -0,0 +1,87 @@ +import { Box, BoxProps, Theme, useTheme } from '@mui/material' +import { useState, useEffect, useMemo } from 'react' + +type MediaQueryProps = BoxProps<'div'> & { + query: string | ((theme: Theme) => string) + children: React.ReactNode +} + +/** + * MediaQuery: Render (and hydrate) a Component based on a media query given. + * + * Example: + * + * ```tsx + * theme.breakpoints.up('md')}> + * Only visisble on desktop + * + * ``` + * + * When to use: + * + * 1. useMediaQuery: When you are now using useMediaQuery to conditionally render content for mobile or desktop. + * a. Is very slow as it has to wait for the JS to initialize on pageload. + * b. Can cause CLS problems if the useMediaQuery is used to render elements in the viewport. + * c. Can cause LCP issues if useMediaQuery is used to render the LCP element. + * d. Causes TBT problems as a component always needs to be rerendered. (And bad TBT can cause INP problems) + * e. HTML isn't present in the DOM, which can cause SEO issues. + * + * 2. CSS Media query: When you are using CSS to show or hide content based on media queries. + * a. Causes TBT problems as both code paths need to be rendered. (And bad TBT can cause INP problems) + * + * How does it work? + * It wraps the component in a div that has 'display: contents;' when shown and 'display: none;' when hidden so it should not interfere with other styling. + * It conditionally hydrates the component if the query matches. If it doesn't match, it will NOT render the component (and thus not execute the JS). + */ +export function MediaQuery(props: MediaQueryProps) { + const { query, sx, children, ...elementProps } = props + + const theme = useTheme() + const queryString = typeof query === 'function' ? query(theme) : query + + const matchMedia = useMemo( + () => globalThis.matchMedia?.(queryString.replace(/^@media( ?)/m, '')), + [queryString], + ) + + const [matches, setMatches] = useState(matchMedia?.matches || false) + + useEffect(() => { + if (matchMedia.matches) setMatches(true) + + const controller = new AbortController() + matchMedia.addEventListener( + 'change', + (e) => { + if (e.matches) setMatches(true) + }, + { signal: controller.signal, once: true }, + ) + return () => controller.abort() + }, [matchMedia]) + + const sxVal = [ + { display: 'none' }, + { [queryString]: { display: 'contents' } }, + ...(Array.isArray(sx) ? sx : [sx]), + ] + + if (typeof window === 'undefined' || matches) { + return ( + + {children} + + ) + } + + return ( + + ) +} diff --git a/packages/next-ui/MediaQuery/index.ts b/packages/next-ui/MediaQuery/index.ts new file mode 100644 index 0000000000..c61558fa61 --- /dev/null +++ b/packages/next-ui/MediaQuery/index.ts @@ -0,0 +1 @@ +export * from './MediaQuery' diff --git a/packages/next-ui/index.ts b/packages/next-ui/index.ts index 19ef7311b6..46b5c3e30b 100644 --- a/packages/next-ui/index.ts +++ b/packages/next-ui/index.ts @@ -34,6 +34,7 @@ export * from './LayoutDefault' export * from './LayoutOverlay' export * from './LayoutParts' export * from './LazyHydrate' +export * from './MediaQuery' export * from './Navigation' export * from './Overlay' export * from './OverlayOrPopperChip'