import { useCallback, useSyncExternalStore } from 'react' /** * Singleton manager for media query listeners. * Prevents adding duplicate listeners for the same query string. */ class MediaQueryManager { private queries = new Map< string, { mql: MediaQueryList listeners: Set<() => void> unsubscribe: () => void } >() public subscribe(query: string, callback: () => void): () => void { if (typeof window === 'undefined') { return () => {} } if (!this.queries.has(query)) { const mql = window.matchMedia(query) const listeners = new Set<() => void>() const handleChange = () => { listeners.forEach((l) => l()) } mql.addEventListener('change', handleChange) this.queries.set(query, { mql, listeners, unsubscribe: () => { mql.removeEventListener('change', handleChange) }, }) } const entry = this.queries.get(query)! entry.listeners.add(callback) return () => { entry.listeners.delete(callback) if (entry.listeners.size === 0) { entry.unsubscribe() this.queries.delete(query) } } } public getSnapshot(query: string): boolean { if (typeof window === 'undefined') return false // If we have an active listener, use its mql instance to avoid creating new ones if (this.queries.has(query)) { return this.queries.get(query)!.mql.matches } return window.matchMedia(query).matches } } const mediaQueryManager = new MediaQueryManager() /** * Optimized hook for tracking media queries with SSR support. * * @param query - The media query string to watch (e.g. "(min-width: 768px)") * @param serverFallback - The value to return during server-side rendering and initial hydration. * Defaults to `false`. Set to `true` if you want to assume the query matches on the server * (e.g. assuming "Desktop" view to avoid flicker). Set to `null` to return `null` until * hydrated (prevents hydration mismatches but causes content to appear after hydration). */ export function useMediaQuery( query: string, serverFallback: boolean | null = false, ): boolean | null { // Memoize the subscribe function to prevent re-subscription on every render const subscribe = useCallback( (callback: () => void) => { return mediaQueryManager.subscribe(query, callback) }, [query], ) const getSnapshot = () => { return mediaQueryManager.getSnapshot(query) } const getServerSnapshot = () => { if (serverFallback === null) { return null } return serverFallback } return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) } /** * Hook that returns true when the screen width is 768px or smaller. * * @deprecated The name `useIsSmallMediaQuery` is misleading: it checks for screens below the "small" breakpoint (max-width: 768px), * which is effectively a "xs" breakpoint. Use `useViewport` hook instead. */ export const useIsSmallMediaQuery = () => useMediaQuery('(max-width: 420px)') /** * Hook that returns true when the screen width is 768px or smaller. * * @deprecated The name `useIsMediumMediaQuery` is misleading: it checks for screens below the "medium" breakpoint (max-width: 768px), * which is effectively a "small" breakpoint. Use `useViewport` hook instead. */ export const useIsMediumMediaQuery = () => useMediaQuery('(max-width: 768px)')