-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathuseMediaQuery.js
More file actions
74 lines (57 loc) · 2.37 KB
/
useMediaQuery.js
File metadata and controls
74 lines (57 loc) · 2.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import { noop } from '@barso/helpers';
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
/**
* @typedef {Object} UseMediaQueryOptions
* @property {number} [debounceMs] - Delay in ms before updating the state.
* @property {boolean | (() => boolean)} [fallback=false] - Initial state used during SSR and hydration.
* @property {(matches: boolean) => void} [onChange] - Callback fired when the query match state changes.
*/
/**
* A robust React hook to monitor media queries, optimized for Next.js and React 18+.
* @param {string} query - The media query string to monitor (e.g., '(max-width: 768px)').
* @param {UseMediaQueryOptions} [options] - Optional configuration for debounce, SSR fallback, and change events.
* @returns {boolean} - Returns true if the media query matches, false otherwise.
*/
export function useMediaQuery(query, { debounceMs, fallback = false, onChange } = {}) {
const getServerSnapshot = () => (typeof fallback === 'function' ? fallback() : fallback);
const mql = useMemo(() => {
if (typeof window === 'undefined' || !window.matchMedia) {
return { matches: getServerSnapshot(), isFallback: true };
}
return window.matchMedia(query);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query]);
const timeoutRef = useRef();
const subscribe = useCallback(
(notify) => {
if (mql.isFallback) return noop;
const handleChange = () => {
if (!debounceMs) return notify();
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(notify, debounceMs);
};
if (!mql.addEventListener) {
mql.addListener?.(handleChange);
return () => {
mql.removeListener?.(handleChange);
clearTimeout(timeoutRef.current);
};
}
mql.addEventListener('change', handleChange);
return () => {
mql.removeEventListener('change', handleChange);
clearTimeout(timeoutRef.current);
};
},
[debounceMs, mql],
);
const getSnapshot = () => mql.matches;
const matches = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
const lastValueRef = useRef(matches);
useEffect(() => {
if (typeof onChange !== 'function' || matches === lastValueRef.current) return;
lastValueRef.current = matches;
onChange(matches);
}, [matches, onChange]);
return matches;
}