@oxyhq/services
Version:
Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀
397 lines (330 loc) • 10.2 kB
text/typescript
/**
* React hook utilities for common patterns and state management
*/
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
/**
* Hook for managing async operations with loading, error, and data states
*/
export function useAsync<T>(
asyncFn: () => Promise<T>,
deps: React.DependencyList = []
) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const execute = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await asyncFn();
setData(result);
return result;
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
throw error;
} finally {
setLoading(false);
}
}, deps);
return { data, loading, error, execute };
}
/**
* Hook for managing async operations that execute on mount
*/
export function useAsyncEffect<T>(
asyncFn: () => Promise<T>,
deps: React.DependencyList = []
) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let mounted = true;
const execute = async () => {
try {
const result = await asyncFn();
if (mounted) {
setData(result);
setLoading(false);
}
} catch (err) {
if (mounted) {
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
setLoading(false);
}
}
};
execute();
return () => {
mounted = false;
};
}, deps);
return { data, loading, error };
}
/**
* Hook for debounced values
*/
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
/**
* Hook for throttled values
*/
export function useThrottle<T>(value: T, delay: number): T {
const [throttledValue, setThrottledValue] = useState<T>(value);
const lastRun = useRef(Date.now());
useEffect(() => {
const handler = setTimeout(() => {
if (Date.now() - lastRun.current >= delay) {
setThrottledValue(value);
lastRun.current = Date.now();
}
}, delay - (Date.now() - lastRun.current));
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return throttledValue;
}
/**
* Hook for previous value
*/
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
});
return ref.current;
}
/**
* Hook for boolean state with toggle
*/
export function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse, setValue };
}
/**
* Hook for counter state
*/
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => setCount(c => c + 1), []);
const decrement = useCallback(() => setCount(c => c - 1), []);
const reset = useCallback(() => setCount(initialValue), [initialValue]);
const setValue = useCallback((value: number) => setCount(value), []);
return { count, increment, decrement, reset, setValue };
}
/**
* Hook for local storage
*/
export function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
const setValue = useCallback((value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
}, [key, storedValue]);
return [storedValue, setValue] as const;
}
/**
* Hook for session storage
*/
export function useSessionStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.sessionStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading sessionStorage key "${key}":`, error);
return initialValue;
}
});
const setValue = useCallback((value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting sessionStorage key "${key}":`, error);
}
}, [key, storedValue]);
return [storedValue, setValue] as const;
}
/**
* Hook for window size
*/
export function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowSize;
}
/**
* Hook for scroll position
*/
export function useScrollPosition() {
const [scrollPosition, setScrollPosition] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollPosition(window.pageYOffset);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return scrollPosition;
}
/**
* Hook for online/offline status
*/
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
/**
* Hook for media queries
*/
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
if (media.matches !== matches) {
setMatches(media.matches);
}
const listener = () => setMatches(media.matches);
media.addEventListener('change', listener);
return () => media.removeEventListener('change', listener);
}, [matches, query]);
return matches;
}
/**
* Hook for keyboard events
*/
export function useKeyPress(targetKey: string): boolean {
const [keyPressed, setKeyPressed] = useState(false);
useEffect(() => {
const downHandler = ({ key }: KeyboardEvent) => {
if (key === targetKey) {
setKeyPressed(true);
}
};
const upHandler = ({ key }: KeyboardEvent) => {
if (key === targetKey) {
setKeyPressed(false);
}
};
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
}, [targetKey]);
return keyPressed;
}
/**
* Hook for click outside detection
*/
export function useClickOutside(ref: React.RefObject<HTMLElement>, handler: () => void) {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
if (!ref.current || ref.current.contains(event.target as Node)) {
return;
}
handler();
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
}
/**
* Hook for form validation
*/
export function useFormValidation<T extends Record<string, unknown>>(
initialValues: T,
validationSchema: (values: T) => Partial<Record<keyof T, string>>
) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
const validate = useCallback((valuesToValidate: T) => {
return validationSchema(valuesToValidate);
}, [validationSchema]);
const setValue = useCallback((field: keyof T, value: T[keyof T]) => {
setValues(prev => ({ ...prev, [field]: value }));
if (touched[field]) {
const newErrors = validate({ ...values, [field]: value });
setErrors(prev => ({ ...prev, [field]: newErrors[field] }));
}
}, [values, touched, validate]);
const setTouchedField = useCallback((field: keyof T) => {
setTouched(prev => ({ ...prev, [field]: true }));
const newErrors = validate(values);
setErrors(prev => ({ ...prev, [field]: newErrors[field] }));
}, [values, validate]);
const isValid = useMemo(() => {
const validationErrors = validate(values);
return Object.keys(validationErrors).length === 0;
}, [values, validate]);
return {
values,
errors,
touched,
isValid,
setValue,
setTouchedField,
setValues,
setErrors,
setTouched
};
}