@jbpark/use-hooks 4.0.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -101,6 +101,7 @@ function MyComponent() {
101
101
  | `useRecursiveTimeout` | 비동기/동기 콜백을 재귀적으로 스케줄링 |
102
102
  | `useMergedRef` | 여러 객체/콜백 ref를 하나의 콜백 ref로 병합 |
103
103
  | `useImage` | 이미지 사전로드 및 `loading`/`error`(`Error`)/`loaded`/`attemptCount`/`retry` 노출 |
104
+ | `useMediaQuery` | `matchMedia`로 CSS 미디어 쿼리 추적 (SSR 안전, iframe/포털 창을 위한 `target` 옵션) |
104
105
 
105
106
  ## v2에서 마이그레이션
106
107
 
package/README.md CHANGED
@@ -101,6 +101,7 @@ function MyComponent() {
101
101
  | `useRecursiveTimeout` | Recursively schedule async/sync callbacks |
102
102
  | `useMergedRef` | Merge multiple object/callback refs into one callback ref |
103
103
  | `useImage` | Preload an image and expose `loading`/`error` (an `Error`)/`loaded`/`attemptCount`/`retry` |
104
+ | `useMediaQuery` | Track a CSS media query via `matchMedia` (SSR-safe, optional `target` for iframe/portal windows) |
104
105
 
105
106
  ## Migrating from v2
106
107
 
@@ -1,4 +1,4 @@
1
- import useDebouncedCallback from "./use-debounced-callback.mjs";
1
+ import useDebouncedCallback, { DebouncedCallback } from "./use-debounced-callback.mjs";
2
2
  import useDebouncedValue from "./use-debounced-value.mjs";
3
3
  import useBodyScrollLock from "./use-body-scroll-lock.mjs";
4
4
  import useClickOutside from "./use-click-outside.mjs";
@@ -15,6 +15,7 @@ import useResponsiveSize from "./use-responsive-size.mjs";
15
15
  import useImage from "./use-image.mjs";
16
16
  import useKeyPress from "./use-key-press.mjs";
17
17
  import useLocalStorage from "./use-local-storage.mjs";
18
+ import useMediaQuery from "./use-media-query.mjs";
18
19
  import useMergedRef from "./use-merged-ref.mjs";
19
20
  import useMultiSelect from "./use-multi-select.mjs";
20
21
  import useMutationObserver from "./use-mutation-observer.mjs";
@@ -28,4 +29,4 @@ import useTimeout from "./use-timeout.mjs";
28
29
  import useToggle from "./use-toggle.mjs";
29
30
  import useWindowScroll from "./use-window-scroll.mjs";
30
31
  import useViewport from "./use-viewport.mjs";
31
- export { useBodyScrollLock, useClickOutside, useControllableState, useDebouncedCallback as useDebounce, useDebouncedCallback, useDebouncedValue, useElementPosition, useElementScroll, useEventListener, useFileDrop, useFileToDataUrl, useHistoryState, useImage, useIntersectionObserver, useInterval, useKeyPress, useLocalStorage, useMergedRef, useMultiSelect, useMutationObserver, usePrevious, useRecursiveTimeout, useResizeObserver, useResponsiveSize, useScrollToElements, useThrottledValue as useThrottle, useThrottledCallback, useThrottledValue, useTimeout, useToggle, useViewport, useWindowScroll };
32
+ export { type DebouncedCallback, useBodyScrollLock, useClickOutside, useControllableState, useDebouncedCallback as useDebounce, useDebouncedCallback, useDebouncedValue, useElementPosition, useElementScroll, useEventListener, useFileDrop, useFileToDataUrl, useHistoryState, useImage, useIntersectionObserver, useInterval, useKeyPress, useLocalStorage, useMediaQuery, useMergedRef, useMultiSelect, useMutationObserver, usePrevious, useRecursiveTimeout, useResizeObserver, useResponsiveSize, useScrollToElements, useThrottledValue as useThrottle, useThrottledCallback, useThrottledValue, useTimeout, useToggle, useViewport, useWindowScroll };
@@ -15,6 +15,7 @@ import useResponsiveSize from "./use-responsive-size.mjs";
15
15
  import useImage from "./use-image.mjs";
16
16
  import useKeyPress from "./use-key-press.mjs";
17
17
  import useLocalStorage from "./use-local-storage.mjs";
18
+ import useMediaQuery from "./use-media-query.mjs";
18
19
  import useMergedRef from "./use-merged-ref.mjs";
19
20
  import useMultiSelect from "./use-multi-select.mjs";
20
21
  import useMutationObserver from "./use-mutation-observer.mjs";
@@ -28,4 +29,4 @@ import useTimeout from "./use-timeout.mjs";
28
29
  import useToggle from "./use-toggle.mjs";
29
30
  import useWindowScroll from "./use-window-scroll.mjs";
30
31
  import useViewport from "./use-viewport.mjs";
31
- export { useBodyScrollLock, useClickOutside, useControllableState, useDebouncedCallback as useDebounce, useDebouncedCallback, useDebouncedValue, useElementPosition, useElementScroll, useEventListener, useFileDrop, useFileToDataUrl, useHistoryState, useImage, useIntersectionObserver, useInterval, useKeyPress, useLocalStorage, useMergedRef, useMultiSelect, useMutationObserver, usePrevious, useRecursiveTimeout, useResizeObserver, useResponsiveSize, useScrollToElements, useThrottledValue as useThrottle, useThrottledCallback, useThrottledValue, useTimeout, useToggle, useViewport, useWindowScroll };
32
+ export { useBodyScrollLock, useClickOutside, useControllableState, useDebouncedCallback as useDebounce, useDebouncedCallback, useDebouncedValue, useElementPosition, useElementScroll, useEventListener, useFileDrop, useFileToDataUrl, useHistoryState, useImage, useIntersectionObserver, useInterval, useKeyPress, useLocalStorage, useMediaQuery, useMergedRef, useMultiSelect, useMutationObserver, usePrevious, useRecursiveTimeout, useResizeObserver, useResponsiveSize, useScrollToElements, useThrottledValue as useThrottle, useThrottledCallback, useThrottledValue, useTimeout, useToggle, useViewport, useWindowScroll };
@@ -3,8 +3,14 @@ interface Options {
3
3
  delay?: number;
4
4
  autoInvoke?: boolean;
5
5
  leading?: boolean;
6
+ flushOnUnmount?: boolean;
6
7
  }
7
- declare const useDebouncedCallback: (callback: () => unknown, { delay, autoInvoke, leading }: Options, deps?: React.DependencyList) => () => void;
8
+ interface DebouncedCallback {
9
+ (): void;
10
+ flush: () => void;
11
+ cancel: () => void;
12
+ }
13
+ declare const useDebouncedCallback: (callback: () => unknown, { delay, autoInvoke, leading, flushOnUnmount }: Options, deps?: React.DependencyList) => DebouncedCallback;
8
14
  //#endregion
9
- export { useDebouncedCallback as default };
15
+ export { DebouncedCallback, useDebouncedCallback as default };
10
16
  //# sourceMappingURL=use-debounced-callback.d.mts.map
@@ -1,23 +1,42 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
2
  //#region src/hooks/use-debounced-callback.ts
3
- const useDebouncedCallback = (callback, { delay = 100, autoInvoke = true, leading = true }, deps = []) => {
3
+ const useDebouncedCallback = (callback, { delay = 100, autoInvoke = true, leading = true, flushOnUnmount = false }, deps = []) => {
4
4
  const timeoutRef = useRef(null);
5
5
  const callbackRef = useRef(callback);
6
6
  const delayRef = useRef(delay);
7
+ const flushOnUnmountRef = useRef(flushOnUnmount);
7
8
  const prevDeps = useRef(void 0);
8
9
  useEffect(() => {
9
10
  callbackRef.current = callback;
10
11
  delayRef.current = delay;
12
+ flushOnUnmountRef.current = flushOnUnmount;
11
13
  });
12
- const [stableDebouncedCallback] = useState(() => () => {
14
+ const [cancel] = useState(() => () => {
15
+ if (timeoutRef.current) {
16
+ clearTimeout(timeoutRef.current);
17
+ timeoutRef.current = null;
18
+ }
19
+ });
20
+ const [flush] = useState(() => () => {
21
+ if (!timeoutRef.current) return;
22
+ clearTimeout(timeoutRef.current);
23
+ timeoutRef.current = null;
24
+ callbackRef.current();
25
+ });
26
+ const [schedule] = useState(() => () => {
13
27
  if (timeoutRef.current) clearTimeout(timeoutRef.current);
14
28
  timeoutRef.current = setTimeout(() => {
29
+ timeoutRef.current = null;
15
30
  callbackRef.current();
16
31
  }, delayRef.current);
17
32
  });
33
+ const [stableDebouncedCallback] = useState(() => Object.assign(() => schedule(), {
34
+ flush,
35
+ cancel
36
+ }));
18
37
  useEffect(() => {
19
38
  const depsChanged = prevDeps.current === void 0 || prevDeps.current.length !== deps.length || prevDeps.current.some((dep, i) => dep !== deps[i]);
20
- if (depsChanged && timeoutRef.current) clearTimeout(timeoutRef.current);
39
+ if (depsChanged) stableDebouncedCallback.cancel();
21
40
  if (autoInvoke && depsChanged) {
22
41
  if (prevDeps.current === void 0 && leading) callbackRef.current();
23
42
  else stableDebouncedCallback();
@@ -26,9 +45,10 @@ const useDebouncedCallback = (callback, { delay = 100, autoInvoke = true, leadin
26
45
  });
27
46
  useEffect(() => {
28
47
  return () => {
29
- if (timeoutRef.current) clearTimeout(timeoutRef.current);
48
+ if (flushOnUnmountRef.current) stableDebouncedCallback.flush();
49
+ else stableDebouncedCallback.cancel();
30
50
  };
31
- }, []);
51
+ }, [stableDebouncedCallback]);
32
52
  return stableDebouncedCallback;
33
53
  };
34
54
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"use-debounced-callback.mjs","names":[],"sources":["../../src/hooks/use-debounced-callback.ts"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\n\ninterface Options {\n delay?: number;\n autoInvoke?: boolean;\n leading?: boolean;\n}\n\n// Auto-invokes `callback` (debounced by `delay`ms) whenever `deps` change,\n// and also returns a stable debounced version of `callback` for manual use.\n// `callback` intentionally takes no arguments — the deps-triggered\n// auto-invoke has no natural argument to supply, so an arg-taking callback\n// here was a latent type hole (it was always invoked with zero args\n// regardless of what the callback's own signature claimed).\nconst useDebouncedCallback = (\n callback: () => unknown,\n { delay = 100, autoInvoke = true, leading = true }: Options,\n deps: React.DependencyList = [],\n) => {\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const callbackRef = useRef(callback);\n // Read the latest `delay` when the timeout actually fires, instead of\n // baking in whatever `delay` was on the render that created the stable\n // debounced function (which otherwise never updates again).\n const delayRef = useRef(delay);\n const prevDeps = useRef<React.DependencyList | undefined>(undefined);\n\n useEffect(() => {\n callbackRef.current = callback;\n delayRef.current = delay;\n });\n\n // Built once via useState's lazy initializer (runs only on mount) rather\n // than the ref-guarded-by-if pattern this used to use — same \"create\n // once, keep a stable identity\" effect, but without ever reading/writing\n // a ref during render, which react-hooks/refs now disallows even for the\n // otherwise-idempotent lazy-ref-init form (see facebook/react#36896).\n const [stableDebouncedCallback] = useState(() => () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n\n timeoutRef.current = setTimeout(() => {\n callbackRef.current();\n }, delayRef.current);\n });\n\n useEffect(() => {\n const depsChanged =\n prevDeps.current === undefined ||\n prevDeps.current.length !== deps.length ||\n prevDeps.current.some((dep, i) => dep !== deps[i]);\n\n if (depsChanged && timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n\n if (autoInvoke && depsChanged) {\n const isFirstRender = prevDeps.current === undefined;\n\n // `leading` makes the \"first invocation fires immediately, without\n // waiting `delay`ms\" behavior explicit and opt-out-able, instead of\n // an unconditional special case baked into the first render.\n if (isFirstRender && leading) {\n callbackRef.current();\n } else {\n stableDebouncedCallback();\n }\n }\n\n prevDeps.current = deps;\n });\n\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, []);\n\n return stableDebouncedCallback;\n};\n\nexport default useDebouncedCallback;\n"],"mappings":";;AAcA,MAAM,wBACJ,UACA,EAAE,QAAQ,KAAK,aAAa,MAAM,UAAU,QAC5C,OAA6B,CAAC,MAC3B;CACH,MAAM,aAAa,OAA6C,IAAI;CACpE,MAAM,cAAc,OAAO,QAAQ;CAInC,MAAM,WAAW,OAAO,KAAK;CAC7B,MAAM,WAAW,OAAyC,KAAA,CAAS;CAEnE,gBAAgB;EACd,YAAY,UAAU;EACtB,SAAS,UAAU;CACrB,CAAC;CAOD,MAAM,CAAC,2BAA2B,qBAAqB;EACrD,IAAI,WAAW,SACb,aAAa,WAAW,OAAO;EAGjC,WAAW,UAAU,iBAAiB;GACpC,YAAY,QAAQ;EACtB,GAAG,SAAS,OAAO;CACrB,CAAC;CAED,gBAAgB;EACd,MAAM,cACJ,SAAS,YAAY,KAAA,KACrB,SAAS,QAAQ,WAAW,KAAK,UACjC,SAAS,QAAQ,MAAM,KAAK,MAAM,QAAQ,KAAK,EAAE;EAEnD,IAAI,eAAe,WAAW,SAC5B,aAAa,WAAW,OAAO;EAGjC,IAAI,cAAc,aAAa;GAM7B,IALsB,SAAS,YAAY,KAAA,KAKtB,SACnB,YAAY,QAAQ;QAEpB,wBAAwB;EAE5B;EAEA,SAAS,UAAU;CACrB,CAAC;CAED,gBAAgB;EACd,aAAa;GACX,IAAI,WAAW,SACb,aAAa,WAAW,OAAO;EAEnC;CACF,GAAG,CAAC,CAAC;CAEL,OAAO;AACT"}
1
+ {"version":3,"file":"use-debounced-callback.mjs","names":[],"sources":["../../src/hooks/use-debounced-callback.ts"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\n\ninterface Options {\n delay?: number;\n autoInvoke?: boolean;\n leading?: boolean;\n // Run a call that is still waiting when the component unmounts, instead of\n // dropping it. For work that must not be lost when its UI goes away, such\n // as saving the last edit of a field that is being closed.\n flushOnUnmount?: boolean;\n}\n\n// The returned function schedules `callback`. Its methods act on a call that\n// is waiting for the delay to pass: `flush` runs it now, `cancel` drops it.\n// Both do nothing when no call is waiting.\nexport interface DebouncedCallback {\n (): void;\n flush: () => void;\n cancel: () => void;\n}\n\n// Auto-invokes `callback` (debounced by `delay`ms) whenever `deps` change,\n// and also returns a stable debounced version of `callback` for manual use.\n// `callback` intentionally takes no arguments — the deps-triggered\n// auto-invoke has no natural argument to supply, so an arg-taking callback\n// here was a latent type hole (it was always invoked with zero args\n// regardless of what the callback's own signature claimed).\nconst useDebouncedCallback = (\n callback: () => unknown,\n {\n delay = 100,\n autoInvoke = true,\n leading = true,\n flushOnUnmount = false,\n }: Options,\n deps: React.DependencyList = [],\n): DebouncedCallback => {\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const callbackRef = useRef(callback);\n // Read the latest `delay` when the timeout actually fires, instead of\n // baking in whatever `delay` was on the render that created the stable\n // debounced function (which otherwise never updates again).\n const delayRef = useRef(delay);\n const flushOnUnmountRef = useRef(flushOnUnmount);\n const prevDeps = useRef<React.DependencyList | undefined>(undefined);\n\n useEffect(() => {\n callbackRef.current = callback;\n delayRef.current = delay;\n flushOnUnmountRef.current = flushOnUnmount;\n });\n\n // Built once via useState's lazy initializer (runs only on mount) rather\n // than the ref-guarded-by-if pattern this used to use — same \"create\n // once, keep a stable identity\" effect, but without ever reading/writing\n // a ref during render, which react-hooks/refs now disallows even for the\n // otherwise-idempotent lazy-ref-init form (see facebook/react#36896).\n //\n // `timeoutRef` holding a timer is what \"a call is waiting\" means, so it is\n // cleared before the callback runs, whichever way it runs.\n const [cancel] = useState(() => () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n }\n });\n\n const [flush] = useState(() => () => {\n if (!timeoutRef.current) {\n return;\n }\n\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n callbackRef.current();\n });\n\n const [schedule] = useState(() => () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n\n timeoutRef.current = setTimeout(() => {\n timeoutRef.current = null;\n callbackRef.current();\n }, delayRef.current);\n });\n\n // Each piece above is its own lazy initializer that only returns a\n // function: building them inside one initializer that also calls\n // `Object.assign` reads to the refs rule as touching refs during render.\n const [stableDebouncedCallback] = useState<DebouncedCallback>(() =>\n Object.assign(() => schedule(), { flush, cancel }),\n );\n\n useEffect(() => {\n const depsChanged =\n prevDeps.current === undefined ||\n prevDeps.current.length !== deps.length ||\n prevDeps.current.some((dep, i) => dep !== deps[i]);\n\n if (depsChanged) {\n stableDebouncedCallback.cancel();\n }\n\n if (autoInvoke && depsChanged) {\n const isFirstRender = prevDeps.current === undefined;\n\n // `leading` makes the \"first invocation fires immediately, without\n // waiting `delay`ms\" behavior explicit and opt-out-able, instead of\n // an unconditional special case baked into the first render.\n if (isFirstRender && leading) {\n callbackRef.current();\n } else {\n stableDebouncedCallback();\n }\n }\n\n prevDeps.current = deps;\n });\n\n useEffect(() => {\n return () => {\n if (flushOnUnmountRef.current) {\n stableDebouncedCallback.flush();\n } else {\n stableDebouncedCallback.cancel();\n }\n };\n }, [stableDebouncedCallback]);\n\n return stableDebouncedCallback;\n};\n\nexport default useDebouncedCallback;\n"],"mappings":";;AA2BA,MAAM,wBACJ,UACA,EACE,QAAQ,KACR,aAAa,MACb,UAAU,MACV,iBAAiB,SAEnB,OAA6B,CAAC,MACR;CACtB,MAAM,aAAa,OAA6C,IAAI;CACpE,MAAM,cAAc,OAAO,QAAQ;CAInC,MAAM,WAAW,OAAO,KAAK;CAC7B,MAAM,oBAAoB,OAAO,cAAc;CAC/C,MAAM,WAAW,OAAyC,KAAA,CAAS;CAEnE,gBAAgB;EACd,YAAY,UAAU;EACtB,SAAS,UAAU;EACnB,kBAAkB,UAAU;CAC9B,CAAC;CAUD,MAAM,CAAC,UAAU,qBAAqB;EACpC,IAAI,WAAW,SAAS;GACtB,aAAa,WAAW,OAAO;GAC/B,WAAW,UAAU;EACvB;CACF,CAAC;CAED,MAAM,CAAC,SAAS,qBAAqB;EACnC,IAAI,CAAC,WAAW,SACd;EAGF,aAAa,WAAW,OAAO;EAC/B,WAAW,UAAU;EACrB,YAAY,QAAQ;CACtB,CAAC;CAED,MAAM,CAAC,YAAY,qBAAqB;EACtC,IAAI,WAAW,SACb,aAAa,WAAW,OAAO;EAGjC,WAAW,UAAU,iBAAiB;GACpC,WAAW,UAAU;GACrB,YAAY,QAAQ;EACtB,GAAG,SAAS,OAAO;CACrB,CAAC;CAKD,MAAM,CAAC,2BAA2B,eAChC,OAAO,aAAa,SAAS,GAAG;EAAE;EAAO;CAAO,CAAC,CACnD;CAEA,gBAAgB;EACd,MAAM,cACJ,SAAS,YAAY,KAAA,KACrB,SAAS,QAAQ,WAAW,KAAK,UACjC,SAAS,QAAQ,MAAM,KAAK,MAAM,QAAQ,KAAK,EAAE;EAEnD,IAAI,aACF,wBAAwB,OAAO;EAGjC,IAAI,cAAc,aAAa;GAM7B,IALsB,SAAS,YAAY,KAAA,KAKtB,SACnB,YAAY,QAAQ;QAEpB,wBAAwB;EAE5B;EAEA,SAAS,UAAU;CACrB,CAAC;CAED,gBAAgB;EACd,aAAa;GACX,IAAI,kBAAkB,SACpB,wBAAwB,MAAM;QAE9B,wBAAwB,OAAO;EAEnC;CACF,GAAG,CAAC,uBAAuB,CAAC;CAE5B,OAAO;AACT"}
@@ -0,0 +1,11 @@
1
+ import { RefObject } from "react";
2
+ //#region src/hooks/use-media-query.d.ts
3
+ type MediaQueryTarget = RefObject<Element | null> | Element | null;
4
+ interface Options {
5
+ defaultValue?: boolean;
6
+ target?: MediaQueryTarget;
7
+ }
8
+ declare const useMediaQuery: (query: string, options?: Options) => boolean;
9
+ //#endregion
10
+ export { useMediaQuery as default };
11
+ //# sourceMappingURL=use-media-query.d.mts.map
@@ -0,0 +1,32 @@
1
+ import { useCallback, useSyncExternalStore } from "react";
2
+ //#region src/hooks/use-media-query.ts
3
+ const resolveWindow = (target) => {
4
+ if (typeof window === "undefined") return null;
5
+ return (target && "current" in target ? target.current : target)?.ownerDocument.defaultView ?? window;
6
+ };
7
+ const getMediaQueryList = (query, target) => {
8
+ const view = resolveWindow(target);
9
+ return view?.matchMedia ? view.matchMedia(query) : null;
10
+ };
11
+ const useMediaQuery = (query, options = {}) => {
12
+ const { defaultValue = false, target } = options;
13
+ const subscribe = useCallback((onStoreChange) => {
14
+ const mql = getMediaQueryList(query, target);
15
+ if (!mql) return () => {};
16
+ mql.addEventListener("change", onStoreChange);
17
+ return () => {
18
+ mql.removeEventListener("change", onStoreChange);
19
+ };
20
+ }, [query, target]);
21
+ const getSnapshot = useCallback(() => getMediaQueryList(query, target)?.matches ?? defaultValue, [
22
+ query,
23
+ target,
24
+ defaultValue
25
+ ]);
26
+ const getServerSnapshot = useCallback(() => defaultValue, [defaultValue]);
27
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
28
+ };
29
+ //#endregion
30
+ export { useMediaQuery as default };
31
+
32
+ //# sourceMappingURL=use-media-query.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-media-query.mjs","names":[],"sources":["../../src/hooks/use-media-query.ts"],"sourcesContent":["import { type RefObject, useCallback, useSyncExternalStore } from 'react';\n\ntype MediaQueryTarget = RefObject<Element | null> | Element | null;\n\ninterface Options {\n // Returned while `matchMedia` is unreachable — during SSR and on the\n // hydration render, where guessing wrong only costs one corrected\n // render once the real value is readable.\n defaultValue?: boolean;\n // Pass a ref to an element rendered inside the window the query should\n // be evaluated against (e.g. a node portaled into an iframe preview,\n // whose viewport is not the host one). Defaults to the host `window`.\n target?: MediaQueryTarget;\n}\n\nconst resolveWindow = (target: MediaQueryTarget | undefined): Window | null => {\n if (typeof window === 'undefined') {\n return null;\n }\n\n const element = target && 'current' in target ? target.current : target;\n\n return element?.ownerDocument.defaultView ?? window;\n};\n\nconst getMediaQueryList = (\n query: string,\n target: MediaQueryTarget | undefined,\n) => {\n const view = resolveWindow(target);\n\n return view?.matchMedia ? view.matchMedia(query) : null;\n};\n\nconst useMediaQuery = (query: string, options: Options = {}) => {\n const { defaultValue = false, target } = options;\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n const mql = getMediaQueryList(query, target);\n\n if (!mql) {\n return () => {};\n }\n\n mql.addEventListener('change', onStoreChange);\n\n return () => {\n mql.removeEventListener('change', onStoreChange);\n };\n },\n [query, target],\n );\n\n // A fresh MediaQueryList per read is fine (and cheaper than caching one\n // per query): the snapshot handed to React is the boolean `matches`, so\n // there is no new object identity to churn re-renders.\n const getSnapshot = useCallback(\n () => getMediaQueryList(query, target)?.matches ?? defaultValue,\n [query, target, defaultValue],\n );\n\n const getServerSnapshot = useCallback(() => defaultValue, [defaultValue]);\n\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n};\n\nexport default useMediaQuery;\n"],"mappings":";;AAeA,MAAM,iBAAiB,WAAwD;CAC7E,IAAI,OAAO,WAAW,aACpB,OAAO;CAKT,QAFgB,UAAU,aAAa,SAAS,OAAO,UAAU,OAAA,EAEjD,cAAc,eAAe;AAC/C;AAEA,MAAM,qBACJ,OACA,WACG;CACH,MAAM,OAAO,cAAc,MAAM;CAEjC,OAAO,MAAM,aAAa,KAAK,WAAW,KAAK,IAAI;AACrD;AAEA,MAAM,iBAAiB,OAAe,UAAmB,CAAC,MAAM;CAC9D,MAAM,EAAE,eAAe,OAAO,WAAW;CAEzC,MAAM,YAAY,aACf,kBAA8B;EAC7B,MAAM,MAAM,kBAAkB,OAAO,MAAM;EAE3C,IAAI,CAAC,KACH,aAAa,CAAC;EAGhB,IAAI,iBAAiB,UAAU,aAAa;EAE5C,aAAa;GACX,IAAI,oBAAoB,UAAU,aAAa;EACjD;CACF,GACA,CAAC,OAAO,MAAM,CAChB;CAKA,MAAM,cAAc,kBACZ,kBAAkB,OAAO,MAAM,CAAC,EAAE,WAAW,cACnD;EAAC;EAAO;EAAQ;CAAY,CAC9B;CAEA,MAAM,oBAAoB,kBAAkB,cAAc,CAAC,YAAY,CAAC;CAExE,OAAO,qBAAqB,WAAW,aAAa,iBAAiB;AACvE"}
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import useDebouncedCallback from "./hooks/use-debounced-callback.mjs";
1
+ import useDebouncedCallback, { DebouncedCallback } from "./hooks/use-debounced-callback.mjs";
2
2
  import useDebouncedValue from "./hooks/use-debounced-value.mjs";
3
3
  import useBodyScrollLock from "./hooks/use-body-scroll-lock.mjs";
4
4
  import useClickOutside from "./hooks/use-click-outside.mjs";
@@ -15,6 +15,7 @@ import useResponsiveSize from "./hooks/use-responsive-size.mjs";
15
15
  import useImage from "./hooks/use-image.mjs";
16
16
  import useKeyPress from "./hooks/use-key-press.mjs";
17
17
  import useLocalStorage from "./hooks/use-local-storage.mjs";
18
+ import useMediaQuery from "./hooks/use-media-query.mjs";
18
19
  import useMergedRef from "./hooks/use-merged-ref.mjs";
19
20
  import useMultiSelect from "./hooks/use-multi-select.mjs";
20
21
  import useMutationObserver from "./hooks/use-mutation-observer.mjs";
@@ -29,4 +30,4 @@ import useToggle from "./hooks/use-toggle.mjs";
29
30
  import useWindowScroll from "./hooks/use-window-scroll.mjs";
30
31
  import useViewport from "./hooks/use-viewport.mjs";
31
32
  import "./hooks/index.mjs";
32
- export { useBodyScrollLock, useClickOutside, useControllableState, useDebouncedCallback as useDebounce, useDebouncedCallback, useDebouncedValue, useElementPosition, useElementScroll, useEventListener, useFileDrop, useFileToDataUrl, useHistoryState, useImage, useIntersectionObserver, useInterval, useKeyPress, useLocalStorage, useMergedRef, useMultiSelect, useMutationObserver, usePrevious, useRecursiveTimeout, useResizeObserver, useResponsiveSize, useScrollToElements, useThrottledValue as useThrottle, useThrottledCallback, useThrottledValue, useTimeout, useToggle, useViewport, useWindowScroll };
33
+ export { type DebouncedCallback, useBodyScrollLock, useClickOutside, useControllableState, useDebouncedCallback as useDebounce, useDebouncedCallback, useDebouncedValue, useElementPosition, useElementScroll, useEventListener, useFileDrop, useFileToDataUrl, useHistoryState, useImage, useIntersectionObserver, useInterval, useKeyPress, useLocalStorage, useMediaQuery, useMergedRef, useMultiSelect, useMutationObserver, usePrevious, useRecursiveTimeout, useResizeObserver, useResponsiveSize, useScrollToElements, useThrottledValue as useThrottle, useThrottledCallback, useThrottledValue, useTimeout, useToggle, useViewport, useWindowScroll };
package/dist/index.mjs CHANGED
@@ -15,6 +15,7 @@ import useResponsiveSize from "./hooks/use-responsive-size.mjs";
15
15
  import useImage from "./hooks/use-image.mjs";
16
16
  import useKeyPress from "./hooks/use-key-press.mjs";
17
17
  import useLocalStorage from "./hooks/use-local-storage.mjs";
18
+ import useMediaQuery from "./hooks/use-media-query.mjs";
18
19
  import useMergedRef from "./hooks/use-merged-ref.mjs";
19
20
  import useMultiSelect from "./hooks/use-multi-select.mjs";
20
21
  import useMutationObserver from "./hooks/use-mutation-observer.mjs";
@@ -29,4 +30,4 @@ import useToggle from "./hooks/use-toggle.mjs";
29
30
  import useWindowScroll from "./hooks/use-window-scroll.mjs";
30
31
  import useViewport from "./hooks/use-viewport.mjs";
31
32
  import "./hooks/index.mjs";
32
- export { useBodyScrollLock, useClickOutside, useControllableState, useDebouncedCallback as useDebounce, useDebouncedCallback, useDebouncedValue, useElementPosition, useElementScroll, useEventListener, useFileDrop, useFileToDataUrl, useHistoryState, useImage, useIntersectionObserver, useInterval, useKeyPress, useLocalStorage, useMergedRef, useMultiSelect, useMutationObserver, usePrevious, useRecursiveTimeout, useResizeObserver, useResponsiveSize, useScrollToElements, useThrottledValue as useThrottle, useThrottledCallback, useThrottledValue, useTimeout, useToggle, useViewport, useWindowScroll };
33
+ export { useBodyScrollLock, useClickOutside, useControllableState, useDebouncedCallback as useDebounce, useDebouncedCallback, useDebouncedValue, useElementPosition, useElementScroll, useEventListener, useFileDrop, useFileToDataUrl, useHistoryState, useImage, useIntersectionObserver, useInterval, useKeyPress, useLocalStorage, useMediaQuery, useMergedRef, useMultiSelect, useMutationObserver, usePrevious, useRecursiveTimeout, useResizeObserver, useResponsiveSize, useScrollToElements, useThrottledValue as useThrottle, useThrottledCallback, useThrottledValue, useTimeout, useToggle, useViewport, useWindowScroll };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbpark/use-hooks",
3
- "version": "4.0.2",
3
+ "version": "4.1.0",
4
4
  "description": "A collection of reusable React 19 hooks for common UI and interaction patterns",
5
5
  "keywords": [
6
6
  "react",