react-hook-granth
Version:
A collection of custom React hooks for efficient state management and UI logic.
600 lines (579 loc) • 20.6 kB
JavaScript
;
var react = require('react');
const useCounter = (initialValue) => {
const normalizedInitialValue = initialValue !== null && initialValue !== void 0 ? initialValue : 0;
const [count, setCount] = react.useState(normalizedInitialValue);
const increment = react.useCallback(() => setCount((prev) => prev + 1), []);
const decrement = react.useCallback(() => setCount((prev) => prev - 1), []);
const reset = react.useCallback(() => setCount(normalizedInitialValue), [normalizedInitialValue]);
return { count, increment, decrement, reset };
};
/**
* Custom hook to sync state with localStorage.
* @param key - localStorage key
* @param initialValue - Initial value or function returning initial value
* @returns Tuple of [storedValue, setStoredValue]
*/
function useLocalStorage(key, initialValue) {
const getSavedValue = () => {
if (typeof window === 'undefined')
return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
}
catch (error) {
console.error('Error reading from localStorage', error);
return initialValue;
}
};
const [storedValue, setStoredValue] = react.useState(getSavedValue);
react.useEffect(() => {
try {
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(storedValue));
}
}
catch (error) {
console.error('Error writing to localStorage', error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
/**
* Sync state with sessionStorage.
* @param key - sessionStorage key
* @param initialValue - Initial value
* @returns Tuple of [value, setValue]
*/
function useSessionStorage(key, initialValue) {
const getValue = () => {
const stored = sessionStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
};
const [value, setValue] = react.useState(getValue);
react.useEffect(() => {
sessionStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
/**
* Store the previous value of a state or prop.
* @param value - The current value
* @returns The previous value
*/
function usePrevious(value) {
const ref = react.useRef();
react.useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// src/hooks/useDebounce.ts
function useDebounce(value, delay = 300, options = {}) {
const { leading = false, trailing = true, maxWait, onDebounce, onCancel, } = options;
const [debouncedValue, setDebouncedValue] = react.useState(value);
const [isPending, setIsPending] = react.useState(false);
const timeoutRef = react.useRef(null);
const maxTimeoutRef = react.useRef(null);
const previousValueRef = react.useRef(value);
const leadingCalledRef = react.useRef(false);
const maxWaitStartTimeRef = react.useRef(null); // Track when maxWait sequence started
const clearTimeouts = react.useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
if (maxTimeoutRef.current) {
clearTimeout(maxTimeoutRef.current);
maxTimeoutRef.current = null;
}
}, []);
const updateValue = react.useCallback((newValue, source = 'debounce') => {
setDebouncedValue(newValue);
setIsPending(false);
clearTimeouts();
if (source === 'debounce' || source === 'maxwait') {
onDebounce === null || onDebounce === void 0 ? void 0 : onDebounce(newValue);
}
previousValueRef.current = newValue;
leadingCalledRef.current = false;
maxWaitStartTimeRef.current = null; // Reset maxWait sequence
}, [onDebounce, clearTimeouts]);
const cancel = react.useCallback(() => {
clearTimeouts();
setIsPending(false);
leadingCalledRef.current = false;
maxWaitStartTimeRef.current = null;
onCancel === null || onCancel === void 0 ? void 0 : onCancel();
}, [clearTimeouts, onCancel]);
const flush = react.useCallback(() => {
if (isPending && timeoutRef.current) {
updateValue(value, 'flush');
}
}, [isPending, value, updateValue]);
react.useEffect(() => {
// If value hasn't changed, do nothing
if (Object.is(previousValueRef.current, value)) {
return;
}
// Initialize maxWait start time on first change in sequence
if (maxWaitStartTimeRef.current === null) {
maxWaitStartTimeRef.current = Date.now();
}
// Clear existing regular timeout (but preserve maxTimeout logic)
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
// Leading edge execution
if (leading && !leadingCalledRef.current) {
setDebouncedValue(value);
leadingCalledRef.current = true;
if (!trailing) {
// Leading only - no pending state
setIsPending(false);
previousValueRef.current = value;
onDebounce === null || onDebounce === void 0 ? void 0 : onDebounce(value);
maxWaitStartTimeRef.current = null;
return;
}
else {
// Leading + trailing - still pending for trailing execution
setIsPending(true);
}
}
else {
setIsPending(true);
}
// Set up trailing edge execution
if (trailing) {
timeoutRef.current = setTimeout(() => {
updateValue(value, 'debounce');
}, delay);
}
// Set up max wait timeout (only if not already set)
if (maxWait && !maxTimeoutRef.current) {
const elapsed = Date.now() - (maxWaitStartTimeRef.current || 0);
const remainingMaxWait = maxWait - elapsed;
if (remainingMaxWait <= 0) {
// MaxWait time has already passed, execute immediately
updateValue(value, 'maxwait');
return;
}
maxTimeoutRef.current = setTimeout(() => {
updateValue(value, 'maxwait');
}, remainingMaxWait);
}
}, [value, delay, leading, trailing, maxWait, updateValue]);
// Reset leading flag when options change
react.useEffect(() => {
leadingCalledRef.current = false;
maxWaitStartTimeRef.current = null;
}, [leading, trailing, delay]);
return {
debouncedValue,
cancel,
flush,
isPending,
};
}
/**
* Throttle a function to only run once every specified delay.
* @param callback - The function to throttle
* @param delay - Delay in milliseconds
* @returns Throttled version of the callback
*/
function useThrottle(callback, delay) {
const lastCallRef = react.useRef(0);
const throttledCallback = react.useCallback((...args) => {
const now = new Date().getTime();
if (now - lastCallRef.current >= delay) {
callback(...args);
lastCallRef.current = now;
}
}, [callback, delay]);
return throttledCallback;
}
/**
* Run a function after a delay. Supports cancel and reset.
* @param callback - Function to run after delay
* @param delay - Delay in milliseconds
* @returns Object with clear and reset functions
*/
function useTimeout(callback, delay) {
const timeoutRef = react.useRef(null);
const savedCallback = react.useRef(callback);
// Update the latest callback if it changes
react.useEffect(() => {
savedCallback.current = callback;
}, [callback]);
const clear = react.useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
}, []);
const reset = react.useCallback(() => {
clear();
timeoutRef.current = setTimeout(() => {
savedCallback.current();
}, delay);
}, [clear, delay]);
react.useEffect(() => {
reset(); // start the timeout
return clear; // cleanup on unmount or delay change
}, [delay, reset, clear]);
return { clear, reset };
}
/**
* Detect if user is idle after a given timeout.
* @param timeout - Timeout in milliseconds (default: 3000)
* @returns True if user is idle
*/
function useIdle(timeout = 3000) {
const [idle, setIdle] = react.useState(false);
react.useEffect(() => {
let timer = null;
const reset = () => {
if (timer)
clearTimeout(timer);
setIdle(false);
timer = setTimeout(() => setIdle(true), timeout);
};
const events = [
'mousemove',
'mousedown',
'keydown',
'scroll',
'touchstart',
];
events.forEach((e) => window.addEventListener(e, reset));
reset(); // start the timer
return () => {
if (timer)
clearTimeout(timer);
events.forEach((e) => window.removeEventListener(e, reset));
};
}, [timeout]);
return idle;
}
/**
* Hook to detect clicks outside of a specified element.
* @param handler - Callback to run on outside click.
* @returns Ref to attach to your target element.
*/
function useClickOutside(handler) {
const ref = react.useRef(null);
react.useEffect(() => {
const listener = (event) => {
// Type guard to ensure event.target exists and is a Node
if (!ref.current ||
!event.target ||
!(event.target instanceof Node) ||
ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [handler]);
return ref;
}
/**
* Custom hook to copy text to clipboard with enhanced features.
* @param options - Configuration options for the hook
* @returns Object with isCopied state, copy function, and reset function
*/
function useCopyToClipboard(options = {}) {
const { resetTime = 2000, onSuccess, onError } = options;
const [isCopied, setIsCopied] = react.useState(false);
const timeoutRef = react.useRef(null);
const reset = react.useCallback(() => {
setIsCopied(false);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
}, []);
const copy = react.useCallback(async (text) => {
if (!(navigator === null || navigator === void 0 ? void 0 : navigator.clipboard)) {
const error = new Error('Clipboard API not supported');
console.warn(error.message);
onError === null || onError === void 0 ? void 0 : onError(error);
return false;
}
try {
await navigator.clipboard.writeText(text);
setIsCopied(true);
onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess();
// Clear existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Set new timeout
timeoutRef.current = setTimeout(() => {
setIsCopied(false);
timeoutRef.current = null;
}, resetTime);
return true;
}
catch (error) {
const copyError = error instanceof Error ? error : new Error('Unknown error occurred');
console.error('Copy failed:', copyError);
onError === null || onError === void 0 ? void 0 : onError(copyError);
setIsCopied(false);
return false;
}
}, [resetTime, onSuccess, onError]);
return { isCopied, copy, reset };
}
/**
* Track the current window size.
* @returns The current window dimensions
*/
function useWindowSize() {
const [size, setSize] = react.useState(() => {
if (typeof window === 'undefined') {
return { width: 0, height: 0 };
}
return {
width: window.innerWidth,
height: window.innerHeight,
};
});
react.useEffect(() => {
const updateSize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener('resize', updateSize);
return () => window.removeEventListener('resize', updateSize);
}, []);
return size;
}
/**
* Custom hook to match a CSS media query.
* @param query - The media query string
* @returns Whether the query currently matches
*/
function useMediaQuery(query) {
const [matches, setMatches] = react.useState(() => {
if (typeof window === 'undefined') {
return false;
}
return window.matchMedia(query).matches;
});
react.useEffect(() => {
if (typeof window === 'undefined') {
return;
}
const mediaQueryList = window.matchMedia(query);
const listener = (event) => setMatches(event.matches);
mediaQueryList.addEventListener('change', listener);
// Initial check
setMatches(mediaQueryList.matches);
return () => {
mediaQueryList.removeEventListener('change', listener);
};
}, [query]);
return matches;
}
/**
* Custom hook to track online/offline status.
* @returns True if online, false if offline
*/
function useOnlineStatus() {
const [isOnline, setIsOnline] = react.useState(() => {
if (typeof navigator === 'undefined') {
return true; // Assume online in SSR
}
return navigator.onLine;
});
react.useEffect(() => {
if (typeof window === 'undefined') {
return;
}
const goOnline = () => setIsOnline(true);
const goOffline = () => setIsOnline(false);
window.addEventListener('online', goOnline);
window.addEventListener('offline', goOffline);
// Clean up
return () => {
window.removeEventListener('online', goOnline);
window.removeEventListener('offline', goOffline);
};
}, []);
return isOnline;
}
/**
* Track the network speed and connection type.
* @returns Network connection details
*/
function useNetworkSpeed() {
const [networkSpeed, setNetworkSpeed] = react.useState({
connectionType: 'unknown',
downlink: 0,
});
react.useEffect(() => {
const updateNetworkSpeed = () => {
if ('connection' in navigator && navigator.connection) {
const { effectiveType, downlink } = navigator.connection;
setNetworkSpeed({ connectionType: effectiveType, downlink });
}
};
updateNetworkSpeed(); // Initial update
window.addEventListener('online', updateNetworkSpeed);
window.addEventListener('offline', updateNetworkSpeed);
return () => {
window.removeEventListener('online', updateNetworkSpeed);
window.removeEventListener('offline', updateNetworkSpeed);
};
}, []);
return networkSpeed;
}
/**
* Track the scroll position of the window.
* @returns The current scroll position (x, y)
*/
function useScrollPosition() {
const [position, setPosition] = react.useState(() => {
if (typeof window === 'undefined') {
return { x: 0, y: 0 };
}
return {
x: window.scrollX,
y: window.scrollY,
};
});
react.useEffect(() => {
if (typeof window === 'undefined') {
return;
}
const handleScroll = () => {
setPosition({ x: window.scrollX, y: window.scrollY });
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return position;
}
/**
* useScrollIntoView Hook
*
* This hook automatically scrolls an element into view when any specified trigger in an array changes.
* It also provides manual control, improved error handling, and optimizations.
*
* @param ref - The React ref object pointing to the target element.
* @param triggers - An array of state variables that trigger the scroll effect when any of them change.
* @param delay - The delay (in milliseconds) before scrolling occurs.
* @param options - ScrollIntoView options.
* @param onScrollComplete - A callback function executed after scrolling is completed.
*
* @returns An object containing:
* - hasScrolled: Indicates whether the scrolling has completed.
* - error: An error object if the ref is invalid.
* - scrollToElement: A function to manually trigger scrolling.
*/
const useScrollIntoView = (ref, triggers = [], delay = 100, options = {
behavior: 'smooth',
block: 'start',
inline: 'nearest',
}, onScrollComplete = () => { }) => {
const [hasScrolled, setHasScrolled] = react.useState(false);
const [error, setError] = react.useState(null);
const timeoutRef = react.useRef(null);
const validateRef = react.useCallback(() => {
if (!ref || !ref.current) {
return { message: 'Invalid ref provided or element not found.' };
}
if (!(ref.current instanceof HTMLElement)) {
return { message: 'Ref is not attached to a valid DOM element.' };
}
return null;
}, [ref]);
const scrollToElement = react.useCallback(() => {
const validationError = validateRef();
if (validationError) {
setError(validationError);
return;
}
setError(null);
setHasScrolled(false);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
var _a;
(_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView(options);
setHasScrolled(true);
onScrollComplete();
}, delay);
}, [validateRef, delay, options, onScrollComplete, ref]);
const triggerDeps = react.useMemo(() => [...triggers], [triggers]);
react.useEffect(() => {
if (!triggers.some(Boolean))
return;
scrollToElement();
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, triggerDeps);
return { hasScrolled, error, scrollToElement };
};
/**
* Set the document title dynamically.
* @param title - The title to set for the document.
*/
function useDocumentTitle(title) {
react.useEffect(() => {
document.title = title;
}, [title]);
}
/**
* Measure the size of a DOM element.
* @returns Tuple of [ref, bounds]
*/
function useMeasure() {
const ref = react.useRef(null);
const [bounds, setBounds] = react.useState({ width: 0, height: 0 });
const measure = react.useCallback(() => {
if (ref.current) {
const { width, height } = ref.current.getBoundingClientRect();
setBounds({ width, height });
}
}, []);
react.useLayoutEffect(() => {
measure();
window.addEventListener('resize', measure);
return () => window.removeEventListener('resize', measure);
}, [measure]);
return [ref, bounds];
}
exports.useClickOutside = useClickOutside;
exports.useCopyToClipboard = useCopyToClipboard;
exports.useCounter = useCounter;
exports.useDebounce = useDebounce;
exports.useDocumentTitle = useDocumentTitle;
exports.useIdle = useIdle;
exports.useLocalStorage = useLocalStorage;
exports.useMeasure = useMeasure;
exports.useMediaQuery = useMediaQuery;
exports.useNetworkSpeed = useNetworkSpeed;
exports.useOnlineStatus = useOnlineStatus;
exports.usePrevious = usePrevious;
exports.useScrollIntoView = useScrollIntoView;
exports.useScrollPosition = useScrollPosition;
exports.useSessionStorage = useSessionStorage;
exports.useThrottle = useThrottle;
exports.useTimeout = useTimeout;
exports.useWindowSize = useWindowSize;
//# sourceMappingURL=index.js.map