msr-hooks 1.0.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.md +1252 -0
- package/package.json +50 -0
- package/src/hooks/index.js +53 -0
- package/src/hooks/useAsyncEffect/index.js +1 -0
- package/src/hooks/useAsyncEffect/useAsyncEffect.d.ts +6 -0
- package/src/hooks/useAsyncEffect/useAsyncEffect.js +31 -0
- package/src/hooks/useChangeIconColor/index.js +1 -0
- package/src/hooks/useChangeIconColor/useChangeIconColor.d.ts +1 -0
- package/src/hooks/useChangeIconColor/useChangeIconColor.js +39 -0
- package/src/hooks/useClickOutsideObject/index.js +1 -0
- package/src/hooks/useClickOutsideObject/useClickOutsideObject.d.ts +8 -0
- package/src/hooks/useClickOutsideObject/useClickOutsideObject.js +28 -0
- package/src/hooks/useClipboard/index.js +1 -0
- package/src/hooks/useClipboard/useClipboard.d.ts +4 -0
- package/src/hooks/useClipboard/useClipboard.js +27 -0
- package/src/hooks/useDebounce/index.js +1 -0
- package/src/hooks/useDebounce/useDebounce.d.ts +1 -0
- package/src/hooks/useDebounce/useDebounce.js +18 -0
- package/src/hooks/useDeepCompareEffect/index.js +1 -0
- package/src/hooks/useDeepCompareEffect/useDeepCompareEffect.d.ts +3 -0
- package/src/hooks/useDeepCompareEffect/useDeepCompareEffect.js +40 -0
- package/src/hooks/useDocumentVisibility/index.js +1 -0
- package/src/hooks/useDocumentVisibility/useDocumentVisibility.d.ts +1 -0
- package/src/hooks/useDocumentVisibility/useDocumentVisibility.js +19 -0
- package/src/hooks/useEffectAfterMount/index.js +1 -0
- package/src/hooks/useEffectAfterMount/useEffectAfterMount.d.ts +4 -0
- package/src/hooks/useEffectAfterMount/useEffectAfterMount.js +20 -0
- package/src/hooks/useElementScrollProgress/index.js +1 -0
- package/src/hooks/useElementScrollProgress/useElementScrollProgress.d.ts +5 -0
- package/src/hooks/useElementScrollProgress/useElementScrollProgress.js +37 -0
- package/src/hooks/useEscapeKey/index.js +1 -0
- package/src/hooks/useEscapeKey/useEscapeKey.d.ts +1 -0
- package/src/hooks/useEscapeKey/useEscapeKey.js +22 -0
- package/src/hooks/useEventListener/index.js +1 -0
- package/src/hooks/useEventListener/useEventListener.d.ts +8 -0
- package/src/hooks/useEventListener/useEventListener.js +25 -0
- package/src/hooks/useFetch/index.js +1 -0
- package/src/hooks/useFetch/useFetch.d.ts +11 -0
- package/src/hooks/useFetch/useFetch.js +35 -0
- package/src/hooks/useHoverIntent/index.js +1 -0
- package/src/hooks/useHoverIntent/useHoverIntent.d.ts +6 -0
- package/src/hooks/useHoverIntent/useHoverIntent.js +81 -0
- package/src/hooks/useIntersectionObserver/index.js +1 -0
- package/src/hooks/useIntersectionObserver/useIntersectionObserver.d.ts +5 -0
- package/src/hooks/useIntersectionObserver/useIntersectionObserver.js +25 -0
- package/src/hooks/useInterval/index.js +1 -0
- package/src/hooks/useInterval/useInterval.d.ts +4 -0
- package/src/hooks/useInterval/useInterval.js +23 -0
- package/src/hooks/useIsomorphicLayoutEffect/index.js +1 -0
- package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.d.ts +3 -0
- package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.js +3 -0
- package/src/hooks/useKeyPressSequence/index.js +1 -0
- package/src/hooks/useKeyPressSequence/useKeyPressSequence.d.ts +5 -0
- package/src/hooks/useKeyPressSequence/useKeyPressSequence.js +52 -0
- package/src/hooks/useKeyboardNavigation/index.js +1 -0
- package/src/hooks/useKeyboardNavigation/useKeyboardNavigation.d.ts +10 -0
- package/src/hooks/useKeyboardNavigation/useKeyboardNavigation.js +50 -0
- package/src/hooks/useLocalStorage/index.js +1 -0
- package/src/hooks/useLocalStorage/useLocalStorage.d.ts +4 -0
- package/src/hooks/useLocalStorage/useLocalStorage.js +44 -0
- package/src/hooks/useLockBodyScroll/index.js +1 -0
- package/src/hooks/useLockBodyScroll/useLockBodyScroll.d.ts +1 -0
- package/src/hooks/useLockBodyScroll/useLockBodyScroll.js +25 -0
- package/src/hooks/useMediaQuery/index.js +1 -0
- package/src/hooks/useMediaQuery/useMediaQuery.d.ts +1 -0
- package/src/hooks/useMediaQuery/useMediaQuery.js +24 -0
- package/src/hooks/useNetworkStatus/index.js +1 -0
- package/src/hooks/useNetworkStatus/useNetworkStatus.d.ts +7 -0
- package/src/hooks/useNetworkStatus/useNetworkStatus.js +47 -0
- package/src/hooks/usePageLeave/index.js +1 -0
- package/src/hooks/usePageLeave/usePageLeave.d.ts +1 -0
- package/src/hooks/usePageLeave/usePageLeave.js +27 -0
- package/src/hooks/useParentWidth/index.js +1 -0
- package/src/hooks/useParentWidth/useParentWidth.d.ts +8 -0
- package/src/hooks/useParentWidth/useParentWidth.js +30 -0
- package/src/hooks/usePortal/index.js +1 -0
- package/src/hooks/usePortal/usePortal.d.ts +1 -0
- package/src/hooks/usePortal/usePortal.js +33 -0
- package/src/hooks/usePrefersReducedMotion/index.js +1 -0
- package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.d.ts +1 -0
- package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.js +24 -0
- package/src/hooks/usePreventZoom/index.js +1 -0
- package/src/hooks/usePreventZoom/usePreventZoom.d.ts +4 -0
- package/src/hooks/usePreventZoom/usePreventZoom.js +39 -0
- package/src/hooks/usePrevious/index.js +1 -0
- package/src/hooks/usePrevious/usePrevious.d.ts +1 -0
- package/src/hooks/usePrevious/usePrevious.js +16 -0
- package/src/hooks/useResize/index.js +1 -0
- package/src/hooks/useResize/useResize.d.ts +16 -0
- package/src/hooks/useResize/useResize.js +32 -0
- package/src/hooks/useSpringValue/index.js +1 -0
- package/src/hooks/useSpringValue/useSpringValue.d.ts +4 -0
- package/src/hooks/useSpringValue/useSpringValue.js +66 -0
- package/src/hooks/useStateHistory/index.js +1 -0
- package/src/hooks/useStateHistory/useStateHistory.d.ts +17 -0
- package/src/hooks/useStateHistory/useStateHistory.js +70 -0
- package/src/hooks/useThrottle/index.js +1 -0
- package/src/hooks/useThrottle/useThrottle.d.ts +1 -0
- package/src/hooks/useThrottle/useThrottle.js +25 -0
- package/src/hooks/useTimeout/index.js +1 -0
- package/src/hooks/useTimeout/useTimeout.d.ts +4 -0
- package/src/hooks/useTimeout/useTimeout.js +22 -0
- package/src/hooks/useToggle/index.js +1 -0
- package/src/hooks/useToggle/useToggle.d.ts +3 -0
- package/src/hooks/useToggle/useToggle.js +16 -0
- package/src/hooks/useUndoRedo/index.js +1 -0
- package/src/hooks/useUndoRedo/useUndoRedo.d.ts +18 -0
- package/src/hooks/useUndoRedo/useUndoRedo.js +49 -0
- package/src/hooks/useWindowSize/index.js +1 -0
- package/src/hooks/useWindowSize/useWindowSize.d.ts +1 -0
- package/src/hooks/useWindowSize/useWindowSize.js +29 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "msr-hooks",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A comprehensive collection of production-ready React hooks for JavaScript and TypeScript projects",
|
|
5
|
+
"private": false,
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"module": "src/index.js",
|
|
8
|
+
"types": "src/index.d.ts",
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"react",
|
|
16
|
+
"hooks",
|
|
17
|
+
"react-hooks",
|
|
18
|
+
"custom-hooks",
|
|
19
|
+
"typescript",
|
|
20
|
+
"javascript",
|
|
21
|
+
"utilities",
|
|
22
|
+
"ui-hooks"
|
|
23
|
+
],
|
|
24
|
+
"author": "MSR",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/Minka1902/msr-hooks"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/Minka1902/msr-hooks/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/Minka1902/msr-hooks#readme",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"lint": "eslint ."
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"react": "^19.2.0",
|
|
42
|
+
"react-dom": "^19.2.0",
|
|
43
|
+
"@eslint/js": "^9.39.1",
|
|
44
|
+
"@types/react": "^19.2.5",
|
|
45
|
+
"@types/react-dom": "^19.2.3",
|
|
46
|
+
"eslint": "^9.39.1",
|
|
47
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
48
|
+
"globals": "^16.5.0"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Core hooks
|
|
2
|
+
export { useEffectAfterMount } from './useEffectAfterMount';
|
|
3
|
+
export { useWindowSize } from './useWindowSize';
|
|
4
|
+
export { useDebounce } from './useDebounce';
|
|
5
|
+
export { usePrevious } from './usePrevious';
|
|
6
|
+
export { useToggle } from './useToggle';
|
|
7
|
+
export { useLocalStorage } from './useLocalStorage';
|
|
8
|
+
|
|
9
|
+
// UI & Interaction hooks
|
|
10
|
+
export { usePreventZoom } from './usePreventZoom';
|
|
11
|
+
export { useChangeIconColor } from './useChangeIconColor';
|
|
12
|
+
export { useClickOutsideObject } from './useClickOutsideObject';
|
|
13
|
+
export { useKeyboardNavigation } from './useKeyboardNavigation';
|
|
14
|
+
export { useEscapeKey } from './useEscapeKey';
|
|
15
|
+
export { useParentWidth } from './useParentWidth';
|
|
16
|
+
export { useResize } from './useResize';
|
|
17
|
+
|
|
18
|
+
// New utility hooks
|
|
19
|
+
export { useMediaQuery } from './useMediaQuery';
|
|
20
|
+
export { useClipboard } from './useClipboard';
|
|
21
|
+
export { useInterval } from './useInterval';
|
|
22
|
+
export { useTimeout } from './useTimeout';
|
|
23
|
+
export { useThrottle } from './useThrottle';
|
|
24
|
+
export { useIntersectionObserver } from './useIntersectionObserver';
|
|
25
|
+
export { useFetch } from './useFetch';
|
|
26
|
+
|
|
27
|
+
// State management & History
|
|
28
|
+
export { useUndoRedo } from './useUndoRedo';
|
|
29
|
+
export { useStateHistory } from './useStateHistory';
|
|
30
|
+
|
|
31
|
+
// Scroll & Animation
|
|
32
|
+
export { useElementScrollProgress } from './useElementScrollProgress';
|
|
33
|
+
export { useSpringValue } from './useSpringValue';
|
|
34
|
+
|
|
35
|
+
// Network & Browser
|
|
36
|
+
export { useNetworkStatus } from './useNetworkStatus';
|
|
37
|
+
export { useDocumentVisibility } from './useDocumentVisibility';
|
|
38
|
+
export { usePageLeave } from './usePageLeave';
|
|
39
|
+
export { usePrefersReducedMotion } from './usePrefersReducedMotion';
|
|
40
|
+
|
|
41
|
+
// DOM & Layout
|
|
42
|
+
export { useLockBodyScroll } from './useLockBodyScroll';
|
|
43
|
+
export { usePortal } from './usePortal';
|
|
44
|
+
|
|
45
|
+
// Events & Interaction
|
|
46
|
+
export { useKeyPressSequence } from './useKeyPressSequence';
|
|
47
|
+
export { useHoverIntent } from './useHoverIntent';
|
|
48
|
+
export { useEventListener } from './useEventListener';
|
|
49
|
+
|
|
50
|
+
// Advanced Effects
|
|
51
|
+
export { useAsyncEffect } from './useAsyncEffect';
|
|
52
|
+
export { useDeepCompareEffect } from './useDeepCompareEffect';
|
|
53
|
+
export { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useAsyncEffect } from './useAsyncEffect';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An async-friendly effect with AbortSignal support.
|
|
5
|
+
* @param {(signal: AbortSignal) => void | (() => void) | Promise<void | (() => void)>} effect
|
|
6
|
+
* @param {React.DependencyList} deps
|
|
7
|
+
*/
|
|
8
|
+
export function useAsyncEffect(effect, deps = []) {
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
let cleanup;
|
|
12
|
+
|
|
13
|
+
const run = async () => {
|
|
14
|
+
const result = await effect(controller.signal);
|
|
15
|
+
if (typeof result === 'function') {
|
|
16
|
+
cleanup = result;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
run().catch(() => {
|
|
21
|
+
// Avoid unhandled rejections; surface errors in the caller if needed.
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return () => {
|
|
25
|
+
controller.abort();
|
|
26
|
+
if (typeof cleanup === 'function') {
|
|
27
|
+
cleanup();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}, deps);
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useChangeIconColor } from './useChangeIconColor';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function useChangeIconColor(color?: string): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Change the favicon color dynamically.
|
|
5
|
+
* @param {string} color - Hex color code for favicon, defaults to #000000
|
|
6
|
+
* @returns {void}
|
|
7
|
+
*/
|
|
8
|
+
export function useChangeIconColor(color = '#000000') {
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const favicon = document.getElementById('favicon');
|
|
11
|
+
if (!favicon) return;
|
|
12
|
+
|
|
13
|
+
const svgContent = `
|
|
14
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-server-cog ">
|
|
15
|
+
<circle cx="12" cy="12" r="3"></circle>
|
|
16
|
+
<path d="M4.5 10H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-.5"></path>
|
|
17
|
+
<path d="M4.5 14H4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2h-.5"></path>
|
|
18
|
+
<path d="M6 6h.01"></path>
|
|
19
|
+
<path d="M6 18h.01"></path>
|
|
20
|
+
<path d="m15.7 13.4-.9-.3"></path>
|
|
21
|
+
<path d="m9.2 10.9-.9-.3"></path>
|
|
22
|
+
<path d="m10.6 15.7.3-.9"></path>
|
|
23
|
+
<path d="m13.6 15.7-.4-1"></path>
|
|
24
|
+
<path d="m10.8 9.3-.4-1"></path>
|
|
25
|
+
<path d="m8.3 13.6 1-.4"></path>
|
|
26
|
+
<path d="m14.7 10.8 1-.4"></path>
|
|
27
|
+
<path d="m13.4 8.3-.3.9"></path>
|
|
28
|
+
</svg>
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const blob = new Blob([svgContent], { type: 'image/svg+xml' });
|
|
33
|
+
const url = URL.createObjectURL(blob);
|
|
34
|
+
favicon.href = url;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.warn('useChangeIconColor: Failed to update favicon', error);
|
|
37
|
+
}
|
|
38
|
+
}, [color]);
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useClickOutsideObject } from './useClickOutsideObject';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect clicks outside an element and trigger a handler.
|
|
5
|
+
* @param {object} ref - React ref to the target element
|
|
6
|
+
* @param {Function} handler - Callback when click occurs outside
|
|
7
|
+
* @param {string} dontReactTo - Element ID to ignore
|
|
8
|
+
* @param {object} excludeRef - Additional ref to exclude from trigger
|
|
9
|
+
* @returns {void}
|
|
10
|
+
*/
|
|
11
|
+
export function useClickOutsideObject(ref, handler, dontReactTo, excludeRef) {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const listener = (event) => {
|
|
14
|
+
const target = event.target;
|
|
15
|
+
if (!ref.current || ref.current.contains(target)) return;
|
|
16
|
+
if (excludeRef?.current && excludeRef.current.contains(target)) return;
|
|
17
|
+
if (event?.target?.id !== dontReactTo) handler();
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
document.addEventListener('mousedown', listener);
|
|
21
|
+
document.addEventListener('touchstart', listener);
|
|
22
|
+
|
|
23
|
+
return () => {
|
|
24
|
+
document.removeEventListener('mousedown', listener);
|
|
25
|
+
document.removeEventListener('touchstart', listener);
|
|
26
|
+
};
|
|
27
|
+
}, [ref, handler, excludeRef, dontReactTo]);
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useClipboard } from './useClipboard';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Copy text to clipboard.
|
|
5
|
+
* @returns {[() => Promise<void>, boolean]} [copy function, isCopied state]
|
|
6
|
+
*/
|
|
7
|
+
export function useClipboard() {
|
|
8
|
+
const [isCopied, setIsCopied] = useState(false);
|
|
9
|
+
|
|
10
|
+
const copy = useCallback(async (text) => {
|
|
11
|
+
if (!navigator?.clipboard) {
|
|
12
|
+
console.warn('Clipboard API not available');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await navigator.clipboard.writeText(text);
|
|
18
|
+
setIsCopied(true);
|
|
19
|
+
setTimeout(() => setIsCopied(false), 2000);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.warn('Failed to copy text', error);
|
|
22
|
+
setIsCopied(false);
|
|
23
|
+
}
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
return [copy, isCopied];
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useDebounce } from './useDebounce';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function useDebounce<T>(value: T, delay?: number): T;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Debounce a changing value.
|
|
5
|
+
* @param {*} value - Value to debounce
|
|
6
|
+
* @param {number} delay - Debounce delay in ms, defaults to 300
|
|
7
|
+
* @returns {*} Debounced value
|
|
8
|
+
*/
|
|
9
|
+
export function useDebounce(value, delay = 300) {
|
|
10
|
+
const [debounced, setDebounced] = useState(value);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const timer = setTimeout(() => setDebounced(value), delay);
|
|
14
|
+
return () => clearTimeout(timer);
|
|
15
|
+
}, [value, delay]);
|
|
16
|
+
|
|
17
|
+
return debounced;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useDeepCompareEffect } from './useDeepCompareEffect';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
function isObject(value) {
|
|
4
|
+
return value !== null && typeof value === 'object';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function deepEqual(a, b) {
|
|
8
|
+
if (Object.is(a, b)) return true;
|
|
9
|
+
if (!isObject(a) || !isObject(b)) return false;
|
|
10
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
11
|
+
|
|
12
|
+
const aKeys = Object.keys(a);
|
|
13
|
+
const bKeys = Object.keys(b);
|
|
14
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
15
|
+
|
|
16
|
+
for (const key of aKeys) {
|
|
17
|
+
if (!bKeys.includes(key)) return false;
|
|
18
|
+
if (!deepEqual(a[key], b[key])) return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* useEffect with deep comparison on dependencies.
|
|
26
|
+
* @param {React.EffectCallback} effect
|
|
27
|
+
* @param {React.DependencyList} deps
|
|
28
|
+
*/
|
|
29
|
+
export function useDeepCompareEffect(effect, deps) {
|
|
30
|
+
const previousDepsRef = useRef();
|
|
31
|
+
const signalRef = useRef(0);
|
|
32
|
+
const dependencyList = deps || [];
|
|
33
|
+
|
|
34
|
+
if (!previousDepsRef.current || !deepEqual(previousDepsRef.current, dependencyList)) {
|
|
35
|
+
previousDepsRef.current = dependencyList;
|
|
36
|
+
signalRef.current += 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
useEffect(effect, [signalRef.current]);
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useDocumentVisibility } from './useDocumentVisibility';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function useDocumentVisibility(): boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Track document visibility (tab focus/blur).
|
|
5
|
+
* @returns {boolean} true when document is visible
|
|
6
|
+
*/
|
|
7
|
+
export function useDocumentVisibility() {
|
|
8
|
+
const isBrowser = typeof document !== 'undefined';
|
|
9
|
+
const [isVisible, setIsVisible] = useState(() => (isBrowser ? !document.hidden : true));
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!isBrowser) return undefined;
|
|
13
|
+
const handler = () => setIsVisible(!document.hidden);
|
|
14
|
+
document.addEventListener('visibilitychange', handler);
|
|
15
|
+
return () => document.removeEventListener('visibilitychange', handler);
|
|
16
|
+
}, [isBrowser]);
|
|
17
|
+
|
|
18
|
+
return isVisible;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useEffectAfterMount } from './useEffectAfterMount';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Run an effect only after the first render (skips mount).
|
|
5
|
+
* @param {Function} effect - Effect callback to run after mount
|
|
6
|
+
* @param {Array} deps - Dependency array, defaults to []
|
|
7
|
+
* @returns {void}
|
|
8
|
+
*/
|
|
9
|
+
export function useEffectAfterMount(effect, deps = []) {
|
|
10
|
+
const isMounted = useRef(false);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!isMounted.current) {
|
|
14
|
+
isMounted.current = true;
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
return effect();
|
|
18
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
19
|
+
}, deps);
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useElementScrollProgress } from './useElementScrollProgress';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Track scroll progress of a specific element (0-1).
|
|
5
|
+
* @param {React.RefObject<HTMLElement>} ref - Element ref to observe
|
|
6
|
+
* @returns {number} progress ratio between 0 and 1
|
|
7
|
+
*/
|
|
8
|
+
export function useElementScrollProgress(ref) {
|
|
9
|
+
const [progress, setProgress] = useState(0);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const el = ref?.current;
|
|
13
|
+
if (!el) return;
|
|
14
|
+
|
|
15
|
+
const handleScroll = () => {
|
|
16
|
+
const max = el.scrollHeight - el.clientHeight;
|
|
17
|
+
if (max <= 0) {
|
|
18
|
+
setProgress(1);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const next = Math.min(1, Math.max(0, el.scrollTop / max));
|
|
22
|
+
setProgress(next);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
handleScroll();
|
|
26
|
+
el.addEventListener('scroll', handleScroll, { passive: true });
|
|
27
|
+
const resizeObserver = new ResizeObserver(handleScroll);
|
|
28
|
+
resizeObserver.observe(el);
|
|
29
|
+
|
|
30
|
+
return () => {
|
|
31
|
+
el.removeEventListener('scroll', handleScroll);
|
|
32
|
+
resizeObserver.disconnect();
|
|
33
|
+
};
|
|
34
|
+
}, [ref]);
|
|
35
|
+
|
|
36
|
+
return progress;
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useEscapeKey } from './useEscapeKey';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function useEscapeKey(handler: () => void): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Trigger a handler when Escape key is pressed.
|
|
5
|
+
* @param {Function} handler - Callback to execute on Escape
|
|
6
|
+
* @returns {void}
|
|
7
|
+
*/
|
|
8
|
+
export function useEscapeKey(handler) {
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const listener = (event) => {
|
|
11
|
+
if (event.key === 'Escape') {
|
|
12
|
+
handler();
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
document.addEventListener('keydown', listener);
|
|
17
|
+
|
|
18
|
+
return () => {
|
|
19
|
+
document.removeEventListener('keydown', listener);
|
|
20
|
+
};
|
|
21
|
+
}, [handler]);
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useEventListener } from './useEventListener';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add an event listener with automatic cleanup.
|
|
5
|
+
* @param {EventTarget | React.RefObject<EventTarget>} target
|
|
6
|
+
* @param {string} type
|
|
7
|
+
* @param {(event: Event) => void} listener
|
|
8
|
+
* @param {boolean | AddEventListenerOptions} [options]
|
|
9
|
+
*/
|
|
10
|
+
export function useEventListener(target, type, listener, options) {
|
|
11
|
+
const handlerRef = useRef(listener);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
handlerRef.current = listener;
|
|
15
|
+
}, [listener]);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const element = target && 'current' in target ? target.current : target || (typeof window !== 'undefined' ? window : undefined);
|
|
19
|
+
if (!element || !element.addEventListener) return undefined;
|
|
20
|
+
|
|
21
|
+
const eventHandler = (event) => handlerRef.current?.(event);
|
|
22
|
+
element.addEventListener(type, eventHandler, options);
|
|
23
|
+
return () => element.removeEventListener(type, eventHandler, options);
|
|
24
|
+
}, [target, type, options]);
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useFetch } from './useFetch';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fetch data with loading and error states.
|
|
5
|
+
* @param {string} url - URL to fetch
|
|
6
|
+
* @param {object} options - Fetch options
|
|
7
|
+
* @returns {{data: *, loading: boolean, error: Error|null, refetch: Function}}
|
|
8
|
+
*/
|
|
9
|
+
export function useFetch(url, options = {}) {
|
|
10
|
+
const [data, setData] = useState(null);
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
12
|
+
const [error, setError] = useState(null);
|
|
13
|
+
|
|
14
|
+
const fetchData = useCallback(async () => {
|
|
15
|
+
setLoading(true);
|
|
16
|
+
setError(null);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const response = await fetch(url, options);
|
|
20
|
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
21
|
+
const result = await response.json();
|
|
22
|
+
setData(result);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
setError(err);
|
|
25
|
+
} finally {
|
|
26
|
+
setLoading(false);
|
|
27
|
+
}
|
|
28
|
+
}, [url, options]);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
fetchData();
|
|
32
|
+
}, [fetchData]);
|
|
33
|
+
|
|
34
|
+
return { data, loading, error, refetch: fetchData };
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useHoverIntent } from './useHoverIntent';
|