Skip to content
Source
Sign inSign up

useMediaQuery.ts

79 lines2.3 KB

1import { useSyncExternalStore } from "react";
2​
3const BREAKPOINTS = {
4 "2xl": 1536,
5 "3xl": 1600,
6 "4xl": 2000,
7 lg: 1024,
8 md: 768,
9 sm: 640,
10 xl: 1280,
11} as const;
12​
13type Breakpoint = keyof typeof BREAKPOINTS;
14​
15type BreakpointQuery = Breakpoint | `max-${Breakpoint}` | `${Breakpoint}:max-${Breakpoint}`;
16​
17function resolveMin(value: Breakpoint | number): string {
18 const px = typeof value === "number" ? value : BREAKPOINTS[value];
19 return `(min-width: ${px}px)`;
21​
22function resolveMax(value: Breakpoint | number): string {
23 const px = typeof value === "number" ? value : BREAKPOINTS[value];
24 return `(max-width: ${px - 1}px)`;
26​
27function parseQuery(query: BreakpointQuery | MediaQueryInput | (string & {})): string {
28 if (typeof query !== "string") {
29 const parts: string[] = [];
30 if (query.min != null) parts.push(resolveMin(query.min));
31 if (query.max != null) parts.push(resolveMax(query.max));
32 if (query.pointer === "coarse") parts.push("(pointer: coarse)");
33 if (query.pointer === "fine") parts.push("(pointer: fine)");
34 if (parts.length === 0) return "(min-width: 0px)";
35 return parts.join(" and ");
36 }
37​
38 if (query.startsWith("(")) return query;
39​
40 const parts: string[] = [];
41 for (const segment of query.split(":")) {
42 if (segment.startsWith("max-")) {
43 const bp = segment.slice(4);
44 if (bp in BREAKPOINTS) parts.push(resolveMax(bp as Breakpoint));
45 } else if (segment in BREAKPOINTS) {
46 parts.push(resolveMin(segment as Breakpoint));
47 }
48 }
49​
50 return parts.length > 0 ? parts.join(" and ") : query;
52​
53function getServerSnapshot(): boolean {
54 return false;
56​
57export type MediaQueryInput = {
58 min?: Breakpoint | number;
59 max?: Breakpoint | number;
60 pointer?: "coarse" | "fine";
61};
62​
63export function useMediaQuery(query: BreakpointQuery | MediaQueryInput | (string & {})): boolean {
64 const mediaQuery = parseQuery(query);
65​
66 const subscribe = (callback: () => void) => {
67 if (typeof window === "undefined") return () => {};
68 const mql = window.matchMedia(mediaQuery);
69 mql.addEventListener("change", callback);
70 return () => mql.removeEventListener("change", callback);
71 };
72​
73 const getSnapshot = () => {
74 if (typeof window === "undefined") return false;
75 return window.matchMedia(mediaQuery).matches;
76 };
77​
78 return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);