React Custom Hooks
Reusable hook patterns for common UI scenarios.
Instructions
- useDebounce
function useDebounce<T>(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => { const timer = setTimeout(() => setDebouncedValue(value), delay); return () => clearTimeout(timer); }, [value, delay]);
return debouncedValue; }
// Usage function SearchInput() { const [query, setQuery] = useState(''); const debouncedQuery = useDebounce(query, 300);
useEffect(() => { if (debouncedQuery) { searchAPI(debouncedQuery); } }, [debouncedQuery]); }
- useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) { const [storedValue, setStoredValue] = useState<T>(() => { if (typeof window === 'undefined') return initialValue; try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch { return initialValue; } });
const setValue = (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); } };
return [storedValue, setValue] as const; }
// Usage const [theme, setTheme] = useLocalStorage('theme', 'dark');
- useMediaQuery
function useMediaQuery(query: string): boolean { const [matches, setMatches] = useState(false);
useEffect(() => { const media = window.matchMedia(query); setMatches(media.matches);
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
media.addEventListener('change', listener);
return () => media.removeEventListener('change', listener);
}, [query]);
return matches; }
// Usage function Component() { const isMobile = useMediaQuery('(max-width: 768px)'); const isDark = useMediaQuery('(prefers-color-scheme: dark)'); }
- useClickOutside
function useClickOutside<T extends HTMLElement>( handler: () => void ): RefObject<T> { const ref = useRef<T>(null);
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);
};
}, [handler]);
return ref; }
// Usage function Dropdown() { const [isOpen, setIsOpen] = useState(false); const ref = useClickOutside<HTMLDivElement>(() => setIsOpen(false));
return <div ref={ref}>{isOpen && <Menu />}</div>; }
- useToggle
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 }; }
// Usage const { value: isOpen, toggle, setFalse: close } = useToggle();
- usePrevious
function usePrevious<T>(value: T): T | undefined { const ref = useRef<T>();
useEffect(() => { ref.current = value; }, [value]);
return ref.current; }
// Usage function Counter({ count }: { count: number }) { const prevCount = usePrevious(count); // prevCount is the previous value of count }
- useAsync
interface AsyncState<T> { data: T | null; loading: boolean; error: Error | null; }
function useAsync<T>(asyncFn: () => Promise<T>, deps: unknown[] = []) { const [state, setState] = useState<AsyncState<T>>({ data: null, loading: true, error: null, });
useEffect(() => { setState(s => ({ ...s, loading: true })); asyncFn() .then(data => setState({ data, loading: false, error: null })) .catch(error => setState({ data: null, loading: false, error })); }, deps);
return state; }
// Usage const { data, loading, error } = useAsync(() => fetchUser(id), [id]);
- useCopyToClipboard
function useCopyToClipboard() { const [copiedText, setCopiedText] = useState<string | null>(null);
const copy = async (text: string) => { try { await navigator.clipboard.writeText(text); setCopiedText(text); return true; } catch { setCopiedText(null); return false; } };
return { copiedText, copy }; }
- useEventListener
function useEventListener<K extends keyof WindowEventMap>( eventName: K, handler: (event: WindowEventMap[K]) => void, element: Window | HTMLElement = window ) { const savedHandler = useRef(handler);
useEffect(() => { savedHandler.current = handler; }, [handler]);
useEffect(() => { const listener = (event: Event) => savedHandler.current(event as WindowEventMap[K]); element.addEventListener(eventName, listener); return () => element.removeEventListener(eventName, listener); }, [eventName, element]); }
// Usage useEventListener('scroll', () => console.log('scrolled')); useEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); });
- useIntersectionObserver
function useIntersectionObserver( ref: RefObject<Element>, options?: IntersectionObserverInit ): boolean { const [isIntersecting, setIsIntersecting] = useState(false);
useEffect(() => { if (!ref.current) return;
const observer = new IntersectionObserver(([entry]) => {
setIsIntersecting(entry.isIntersecting);
}, options);
observer.observe(ref.current);
return () => observer.disconnect();
}, [ref, options]);
return isIntersecting; }
// Usage - Lazy loading function LazyImage({ src }: { src: string }) { const ref = useRef<HTMLDivElement>(null); const isVisible = useIntersectionObserver(ref, { threshold: 0.1 });
return ( <div ref={ref}> {isVisible && <img src={src} />} </div> ); }
Best Practices
Do Don't
✅ Prefix with use
❌ Name without use
✅ Return stable references ❌ Return new objects each render
✅ Handle cleanup ❌ Forget cleanup in useEffect
✅ Use TypeScript generics ❌ Use any types
References
-
React Hooks Documentation
-
usehooks-ts