React Custom Hooks Library

A comprehensive collection of battle-tested custom React hooks for common use cases. Copy, customize, and integrate these hooks into your projects to solve real-world problems efficiently.

Advertisement Space - top-hooks

Google AdSense: horizontal

Essential State Management Hooks

State & Logic Hooks

useLocalStorage

Persist state to localStorage with automatic serialization

1// useLocalStorage - Sync state with localStorage
2import { useState, useEffect, useCallback } from 'react';
3
4function useLocalStorage<T>(
5 key: string,
6 initialValue: T,
7 options?: {
8 serializer?: (value: T) => string;
9 deserializer?: (value: string) => T;
10 }
11): [T, (value: T | ((val: T) => T)) => void, () => void] {
12 const {
13 serializer = JSON.stringify,
14 deserializer = JSON.parse
15 } = options || {};
16
17 // Get initial value from localStorage or use provided initial value
18 const [storedValue, setStoredValue] = useState<T>(() => {
19 try {
20 if (typeof window === 'undefined') {
21 return initialValue;
22 }
23
24 const item = window.localStorage.getItem(key);
25 return item ? deserializer(item) : initialValue;
26 } catch (error) {
27 console.error(`Error loading localStorage key "${key}":`, error);
28 return initialValue;
29 }
30 });
31
32 // Update localStorage when state changes
33 const setValue = useCallback((value: T | ((val: T) => T)) => {
34 try {
35 const valueToStore = value instanceof Function ? value(storedValue) : value;
36 setStoredValue(valueToStore);
37
38 if (typeof window !== 'undefined') {
39 window.localStorage.setItem(key, serializer(valueToStore));
40
41 // Dispatch storage event for cross-tab synchronization
42 window.dispatchEvent(new StorageEvent('storage', {
43 key,
44 newValue: serializer(valueToStore),
45 url: window.location.href,
46 storageArea: window.localStorage
47 }));
48 }
49 } catch (error) {
50 console.error(`Error saving to localStorage key "${key}":`, error);
51 }
52 }, [key, serializer, storedValue]);
53
54 // Remove value from localStorage
55 const removeValue = useCallback(() => {
56 try {
57 setStoredValue(initialValue);
58 if (typeof window !== 'undefined') {
59 window.localStorage.removeItem(key);
60 }
61 } catch (error) {
62 console.error(`Error removing localStorage key "${key}":`, error);
63 }
64 }, [key, initialValue]);
65
66 // Listen for changes in other tabs
67 useEffect(() => {
68 const handleStorageChange = (e: StorageEvent) => {
69 if (e.key === key && e.newValue) {
70 try {
71 setStoredValue(deserializer(e.newValue));
72 } catch (error) {
73 console.error(`Error parsing localStorage update for key "${key}":`, error);
74 }
75 }
76 };
77
78 window.addEventListener('storage', handleStorageChange);
79 return () => window.removeEventListener('storage', handleStorageChange);
80 }, [key, deserializer]);
81
82 return [storedValue, setValue, removeValue];
83}
84
85// Usage example
86function Settings() {
87 const [theme, setTheme, resetTheme] = useLocalStorage('theme', 'light');
88 const [preferences, setPreferences] = useLocalStorage('userPrefs', {
89 notifications: true,
90 autoSave: false,
91 language: 'en'
92 });
93
94 return (
95 <div>
96 <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
97 Toggle Theme: {theme}
98 </button>
99
100 <button onClick={() => setPreferences(prev => ({
101 ...prev,
102 notifications: !prev.notifications
103 }))}>
104 Notifications: {preferences.notifications ? 'On' : 'Off'}
105 </button>
106
107 <button onClick={resetTheme}>Reset Theme</button>
108 </div>
109 );
110}

useDebounce

Debounce values to limit update frequency

1// useDebounce - Delay value updates
2import { useState, useEffect } from 'react';
3
4function useDebounce<T>(value: T, delay: number): T {
5 const [debouncedValue, setDebouncedValue] = useState<T>(value);
6
7 useEffect(() => {
8 const handler = setTimeout(() => {
9 setDebouncedValue(value);
10 }, delay);
11
12 return () => {
13 clearTimeout(handler);
14 };
15 }, [value, delay]);
16
17 return debouncedValue;
18}
19
20// Advanced version with callback
21function useDebouncedCallback<T extends (...args: any[]) => any>(
22 callback: T,
23 delay: number,
24 deps: React.DependencyList = []
25): T {
26 const [timer, setTimer] = useState<NodeJS.Timeout>();
27
28 useEffect(() => {
29 return () => {
30 if (timer) clearTimeout(timer);
31 };
32 }, [timer]);
33
34 const debouncedCallback = useCallback((...args: Parameters<T>) => {
35 if (timer) clearTimeout(timer);
36
37 const newTimer = setTimeout(() => {
38 callback(...args);
39 }, delay);
40
41 setTimer(newTimer);
42 }, [callback, delay, ...deps]) as T;
43
44 return debouncedCallback;
45}
46
47// Usage example
48function SearchComponent() {
49 const [searchTerm, setSearchTerm] = useState('');
50 const debouncedSearchTerm = useDebounce(searchTerm, 500);
51
52 const [results, setResults] = useState([]);
53 const [isSearching, setIsSearching] = useState(false);
54
55 // Search when debounced value changes
56 useEffect(() => {
57 if (debouncedSearchTerm) {
58 setIsSearching(true);
59 searchAPI(debouncedSearchTerm).then(results => {
60 setIsSearching(false);
61 setResults(results);
62 });
63 } else {
64 setResults([]);
65 }
66 }, [debouncedSearchTerm]);
67
68 // Using debounced callback
69 const debouncedSearch = useDebouncedCallback(
70 (term: string) => {
71 console.log('Searching for:', term);
72 // Perform search
73 },
74 300
75 );
76
77 return (
78 <div>
79 <input
80 placeholder="Search..."
81 value={searchTerm}
82 onChange={(e) => {
83 setSearchTerm(e.target.value);
84 debouncedSearch(e.target.value);
85 }}
86 />
87 {isSearching && <div>Searching...</div>}
88 {results.map(result => (
89 <div key={result.id}>{result.name}</div>
90 ))}
91 </div>
92 );
93}

useToggle

Simple boolean state toggler with additional controls

1// useToggle - Enhanced boolean state management
2import { useState, useCallback } from 'react';
3
4function useToggle(
5 initialValue: boolean = false
6): [boolean, {
7 toggle: () => void;
8 on: () => void;
9 off: () => void;
10 set: (value: boolean) => void;
11}] {
12 const [value, setValue] = useState(initialValue);
13
14 const toggle = useCallback(() => setValue(v => !v), []);
15 const on = useCallback(() => setValue(true), []);
16 const off = useCallback(() => setValue(false), []);
17 const set = useCallback((newValue: boolean) => setValue(newValue), []);
18
19 return [value, { toggle, on, off, set }];
20}
21
22// Usage example
23function Modal() {
24 const [isOpen, { toggle, on: open, off: close }] = useToggle(false);
25 const [isLoading, loadingControls] = useToggle(false);
26
27 const handleSubmit = async () => {
28 loadingControls.on();
29 try {
30 await submitData();
31 close();
32 } finally {
33 loadingControls.off();
34 }
35 };
36
37 return (
38 <>
39 <button onClick={open}>Open Modal</button>
40
41 {isOpen && (
42 <div className="modal">
43 <div className="modal-content">
44 <h2>Modal Title</h2>
45 <button onClick={close}>×</button>
46
47 <button
48 onClick={handleSubmit}
49 disabled={isLoading}
50 >
51 {isLoading ? 'Submitting...' : 'Submit'}
52 </button>
53 </div>
54 </div>
55 )}
56 </>
57 );
58}

usePrevious

Track previous value of a state or prop

1// usePrevious - Remember previous values
2import { useRef, useEffect } from 'react';
3
4function usePrevious<T>(value: T): T | undefined {
5 const ref = useRef<T>();
6
7 useEffect(() => {
8 ref.current = value;
9 }, [value]);
10
11 return ref.current;
12}
13
14// Advanced version with comparison
15function usePreviousDistinct<T>(
16 value: T,
17 isEqual: (a: T | undefined, b: T) => boolean = (a, b) => a === b
18): T | undefined {
19 const ref = useRef<T>();
20 const prevValue = ref.current;
21
22 if (!isEqual(prevValue, value)) {
23 ref.current = value;
24 }
25
26 return prevValue;
27}
28
29// Usage example
30function Counter() {
31 const [count, setCount] = useState(0);
32 const prevCount = usePrevious(count);
33
34 const [user, setUser] = useState({ name: 'John', age: 30 });
35 const prevUser = usePreviousDistinct(user, (a, b) =>
36 a?.name === b.name && a?.age === b.age
37 );
38
39 return (
40 <div>
41 <h3>
42 Now: {count}, before: {prevCount ?? 'N/A'}
43 </h3>
44 <button onClick={() => setCount(count + 1)}>
45 Increment
46 </button>
47
48 {prevUser && user.name !== prevUser.name && (
49 <p>Name changed from {prevUser.name} to {user.name}</p>
50 )}
51 </div>
52 );
53}

Browser API and DOM Interaction Hooks

DOM & Browser Hooks

useClickOutside

Detect clicks outside of a specific element

1// useClickOutside - Handle outside clicks
2import { useEffect, useRef, RefObject } from 'react';
3
4function useClickOutside<T extends HTMLElement = HTMLElement>(
5 handler: (event: MouseEvent | TouchEvent) => void,
6 mouseEvent: 'mousedown' | 'mouseup' = 'mousedown'
7): RefObject<T> {
8 const ref = useRef<T>(null);
9
10 useEffect(() => {
11 const listener = (event: MouseEvent | TouchEvent) => {
12 const element = ref.current;
13 if (!element || element.contains(event.target as Node)) {
14 return;
15 }
16 handler(event);
17 };
18
19 document.addEventListener(mouseEvent, listener);
20 document.addEventListener('touchstart', listener);
21
22 return () => {
23 document.removeEventListener(mouseEvent, listener);
24 document.removeEventListener('touchstart', listener);
25 };
26 }, [handler, mouseEvent]);
27
28 return ref;
29}
30
31// Alternative version with multiple refs
32function useClickOutsideMultiple(
33 refs: RefObject<HTMLElement>[],
34 handler: (event: MouseEvent | TouchEvent) => void
35) {
36 useEffect(() => {
37 const listener = (event: MouseEvent | TouchEvent) => {
38 const isOutside = refs.every(ref => {
39 const element = ref.current;
40 return !element || !element.contains(event.target as Node);
41 });
42
43 if (isOutside) {
44 handler(event);
45 }
46 };
47
48 document.addEventListener('mousedown', listener);
49 document.addEventListener('touchstart', listener);
50
51 return () => {
52 document.removeEventListener('mousedown', listener);
53 document.removeEventListener('touchstart', listener);
54 };
55 }, [refs, handler]);
56}
57
58// Usage example
59function Dropdown() {
60 const [isOpen, setIsOpen] = useState(false);
61 const dropdownRef = useClickOutside<HTMLDivElement>(() => {
62 setIsOpen(false);
63 });
64
65 return (
66 <div ref={dropdownRef} className="dropdown">
67 <button onClick={() => setIsOpen(!isOpen)}>
68 Toggle Dropdown
69 </button>
70
71 {isOpen && (
72 <div className="dropdown-content">
73 <a href="#">Option 1</a>
74 <a href="#">Option 2</a>
75 <a href="#">Option 3</a>
76 </div>
77 )}
78 </div>
79 );
80}

useIntersectionObserver

Observe element visibility and intersection

1// useIntersectionObserver - Track element visibility
2import { useState, useEffect, useRef, RefObject } from 'react';
3
4interface UseIntersectionObserverOptions extends IntersectionObserverInit {
5 freezeOnceVisible?: boolean;
6}
7
8function useIntersectionObserver<T extends HTMLElement = HTMLElement>(
9 options?: UseIntersectionObserverOptions
10): [RefObject<T>, IntersectionObserverEntry | undefined] {
11 const { threshold = 0, root = null, rootMargin = '0px', freezeOnceVisible = false } = options || {};
12 const [entry, setEntry] = useState<IntersectionObserverEntry>();
13 const [frozen, setFrozen] = useState(false);
14 const ref = useRef<T>(null);
15
16 useEffect(() => {
17 const node = ref.current;
18 if (!node || frozen) return;
19
20 const observer = new IntersectionObserver(
21 ([entry]) => {
22 setEntry(entry);
23 if (freezeOnceVisible && entry.isIntersecting) {
24 setFrozen(true);
25 }
26 },
27 { threshold, root, rootMargin }
28 );
29
30 observer.observe(node);
31
32 return () => observer.disconnect();
33 }, [threshold, root, rootMargin, frozen, freezeOnceVisible]);
34
35 return [ref, entry];
36}
37
38// Advanced lazy loading hook
39function useLazyLoad<T extends HTMLElement = HTMLElement>(
40 onIntersect: () => void,
41 options?: IntersectionObserverInit
42): RefObject<T> {
43 const ref = useRef<T>(null);
44 const callbackRef = useRef(onIntersect);
45 const hasIntersected = useRef(false);
46
47 // Update callback ref
48 useEffect(() => {
49 callbackRef.current = onIntersect;
50 });
51
52 useEffect(() => {
53 const node = ref.current;
54 if (!node || hasIntersected.current) return;
55
56 const observer = new IntersectionObserver(
57 ([entry]) => {
58 if (entry.isIntersecting && !hasIntersected.current) {
59 hasIntersected.current = true;
60 callbackRef.current();
61 observer.disconnect();
62 }
63 },
64 options
65 );
66
67 observer.observe(node);
68
69 return () => observer.disconnect();
70 }, [options]);
71
72 return ref;
73}
74
75// Usage example
76function LazyImage({ src, alt }: { src: string; alt: string }) {
77 const [imageSrc, setImageSrc] = useState<string>('');
78 const [imageRef, entry] = useIntersectionObserver<HTMLImageElement>({
79 threshold: 0.1,
80 rootMargin: '50px'
81 });
82
83 useEffect(() => {
84 if (entry?.isIntersecting) {
85 // Start loading image when it's about to be visible
86 const img = new Image();
87 img.src = src;
88 img.onload = () => setImageSrc(src);
89 }
90 }, [entry, src]);
91
92 return (
93 <div ref={imageRef} className="lazy-image-container">
94 {imageSrc ? (
95 <img src={imageSrc} alt={alt} />
96 ) : (
97 <div className="placeholder" />
98 )}
99 </div>
100 );
101}
102
103// Infinite scroll example
104function InfiniteList() {
105 const [items, setItems] = useState<number[]>([...Array(20).keys()]);
106 const [loading, setLoading] = useState(false);
107
108 const loadMoreRef = useLazyLoad(() => {
109 setLoading(true);
110 // Simulate API call
111 setTimeout(() => {
112 setItems(prev => [...prev, ...Array(20).keys()].map((_, i) => i));
113 setLoading(false);
114 }, 1000);
115 }, { rootMargin: '100px' });
116
117 return (
118 <div>
119 {items.map(item => (
120 <div key={item} className="list-item">
121 Item {item}
122 </div>
123 ))}
124
125 <div ref={loadMoreRef}>
126 {loading ? 'Loading more...' : 'Scroll for more'}
127 </div>
128 </div>
129 );
130}

useWindowSize

Track window dimensions with debouncing

1// useWindowSize - Responsive window dimensions
2import { useState, useEffect } from 'react';
3
4interface WindowSize {
5 width: number;
6 height: number;
7}
8
9function useWindowSize(debounceMs: number = 0): WindowSize {
10 const [windowSize, setWindowSize] = useState<WindowSize>({
11 width: typeof window !== 'undefined' ? window.innerWidth : 0,
12 height: typeof window !== 'undefined' ? window.innerHeight : 0,
13 });
14
15 useEffect(() => {
16 let timeoutId: NodeJS.Timeout;
17
18 const handleResize = () => {
19 const update = () => {
20 setWindowSize({
21 width: window.innerWidth,
22 height: window.innerHeight,
23 });
24 };
25
26 if (debounceMs > 0) {
27 clearTimeout(timeoutId);
28 timeoutId = setTimeout(update, debounceMs);
29 } else {
30 update();
31 }
32 };
33
34 window.addEventListener('resize', handleResize);
35
36 // Call handler right away so state gets updated with initial window size
37 handleResize();
38
39 return () => {
40 window.removeEventListener('resize', handleResize);
41 if (timeoutId) clearTimeout(timeoutId);
42 };
43 }, [debounceMs]);
44
45 return windowSize;
46}
47
48// Media query hook
49function useMediaQuery(query: string): boolean {
50 const [matches, setMatches] = useState(false);
51
52 useEffect(() => {
53 const mediaQuery = window.matchMedia(query);
54
55 // Set initial value
56 setMatches(mediaQuery.matches);
57
58 // Create listener
59 const listener = (event: MediaQueryListEvent) => {
60 setMatches(event.matches);
61 };
62
63 // Add listener (using addEventListener for better browser support)
64 if (mediaQuery.addEventListener) {
65 mediaQuery.addEventListener('change', listener);
66 } else {
67 mediaQuery.addListener(listener);
68 }
69
70 // Cleanup
71 return () => {
72 if (mediaQuery.removeEventListener) {
73 mediaQuery.removeEventListener('change', listener);
74 } else {
75 mediaQuery.removeListener(listener);
76 }
77 };
78 }, [query]);
79
80 return matches;
81}
82
83// Usage example
84function ResponsiveComponent() {
85 const { width, height } = useWindowSize(200); // 200ms debounce
86 const isMobile = useMediaQuery('(max-width: 768px)');
87 const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
88 const isDesktop = useMediaQuery('(min-width: 1025px)');
89 const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
90
91 return (
92 <div>
93 <p>Window size: {width} x {height}</p>
94 <p>Device: {isMobile ? 'Mobile' : isTablet ? 'Tablet' : 'Desktop'}</p>
95 <p>Theme preference: {prefersDark ? 'Dark' : 'Light'}</p>
96
97 {isMobile ? (
98 <MobileLayout />
99 ) : isTablet ? (
100 <TabletLayout />
101 ) : (
102 <DesktopLayout />
103 )}
104 </div>
105 );
106}

useScrollPosition

Track and control scroll position

1// useScrollPosition - Advanced scroll tracking
2import { useState, useEffect, useCallback, useRef } from 'react';
3
4interface ScrollPosition {
5 x: number;
6 y: number;
7 direction: 'up' | 'down' | 'none';
8 isAtTop: boolean;
9 isAtBottom: boolean;
10}
11
12function useScrollPosition(
13 delay: number = 0,
14 element?: HTMLElement | null
15): ScrollPosition {
16 const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({
17 x: 0,
18 y: 0,
19 direction: 'none',
20 isAtTop: true,
21 isAtBottom: false,
22 });
23
24 const previousScrollY = useRef(0);
25 const timeoutRef = useRef<NodeJS.Timeout>();
26
27 const handleScroll = useCallback(() => {
28 const updatePosition = () => {
29 const target = element || window;
30 const scrollY = element ? element.scrollTop : window.scrollY;
31 const scrollX = element ? element.scrollLeft : window.scrollX;
32
33 const scrollHeight = element
34 ? element.scrollHeight
35 : document.documentElement.scrollHeight;
36
37 const clientHeight = element
38 ? element.clientHeight
39 : window.innerHeight;
40
41 const direction = scrollY > previousScrollY.current
42 ? 'down'
43 : scrollY < previousScrollY.current
44 ? 'up'
45 : 'none';
46
47 const isAtTop = scrollY === 0;
48 const isAtBottom = scrollY + clientHeight >= scrollHeight - 1;
49
50 setScrollPosition({
51 x: scrollX,
52 y: scrollY,
53 direction,
54 isAtTop,
55 isAtBottom,
56 });
57
58 previousScrollY.current = scrollY;
59 };
60
61 if (delay > 0) {
62 clearTimeout(timeoutRef.current);
63 timeoutRef.current = setTimeout(updatePosition, delay);
64 } else {
65 updatePosition();
66 }
67 }, [delay, element]);
68
69 useEffect(() => {
70 const target = element || window;
71 target.addEventListener('scroll', handleScroll, { passive: true });
72
73 // Initialize
74 handleScroll();
75
76 return () => {
77 target.removeEventListener('scroll', handleScroll);
78 if (timeoutRef.current) clearTimeout(timeoutRef.current);
79 };
80 }, [handleScroll, element]);
81
82 return scrollPosition;
83}
84
85// Scroll restoration hook
86function useScrollRestoration(key: string) {
87 const scrollPositions = useRef<Map<string, number>>(new Map());
88
89 const saveScrollPosition = useCallback((id: string) => {
90 scrollPositions.current.set(id, window.scrollY);
91 sessionStorage.setItem(`scroll-${key}-${id}`, String(window.scrollY));
92 }, [key]);
93
94 const restoreScrollPosition = useCallback((id: string) => {
95 const savedPosition = sessionStorage.getItem(`scroll-${key}-${id}`);
96 const position = savedPosition
97 ? parseInt(savedPosition, 10)
98 : scrollPositions.current.get(id) || 0;
99
100 window.scrollTo(0, position);
101 }, [key]);
102
103 return { saveScrollPosition, restoreScrollPosition };
104}
105
106// Usage example
107function ScrollAwareHeader() {
108 const { y, direction, isAtTop } = useScrollPosition(50);
109 const [isVisible, setIsVisible] = useState(true);
110
111 useEffect(() => {
112 if (direction === 'down' && y > 100) {
113 setIsVisible(false);
114 } else if (direction === 'up' || isAtTop) {
115 setIsVisible(true);
116 }
117 }, [direction, y, isAtTop]);
118
119 return (
120 <header
121 className={`header ${isVisible ? 'visible' : 'hidden'}`}
122 style={{
123 transform: `translateY(${isVisible ? '0' : '-100%'})`,
124 transition: 'transform 0.3s ease-in-out',
125 position: 'fixed',
126 top: 0,
127 width: '100%',
128 backgroundColor: isAtTop ? 'transparent' : 'white',
129 }}
130 >
131 <nav>Navigation Content</nav>
132 </header>
133 );
134}

API Integration and Data Management Hooks

Data & API Hooks

useFetch

Comprehensive data fetching hook with caching

1// useFetch - Advanced data fetching with caching
2import { useState, useEffect, useCallback, useRef } from 'react';
3
4interface FetchState<T> {
5 data: T | null;
6 error: Error | null;
7 loading: boolean;
8 refetch: () => Promise<void>;
9 mutate: (data: T) => void;
10}
11
12interface FetchOptions extends RequestInit {
13 deps?: any[];
14 cache?: boolean;
15 cacheTime?: number;
16 onSuccess?: (data: any) => void;
17 onError?: (error: Error) => void;
18 transform?: (data: any) => any;
19}
20
21// Simple cache implementation
22const cache = new Map<string, { data: any; timestamp: number }>();
23
24function useFetch<T = any>(
25 url: string | null,
26 options?: FetchOptions
27): FetchState<T> {
28 const [state, setState] = useState<{
29 data: T | null;
30 error: Error | null;
31 loading: boolean;
32 }>({
33 data: null,
34 error: null,
35 loading: true,
36 });
37
38 const {
39 deps = [],
40 cache: useCache = true,
41 cacheTime = 5 * 60 * 1000, // 5 minutes
42 onSuccess,
43 onError,
44 transform = (data: any) => data,
45 ...fetchOptions
46 } = options || {};
47
48 const abortControllerRef = useRef<AbortController>();
49
50 const fetchData = useCallback(async () => {
51 if (!url) {
52 setState({ data: null, error: null, loading: false });
53 return;
54 }
55
56 // Check cache
57 if (useCache && cache.has(url)) {
58 const cached = cache.get(url)!;
59 if (Date.now() - cached.timestamp < cacheTime) {
60 setState({ data: cached.data, error: null, loading: false });
61 return;
62 }
63 }
64
65 // Abort previous request
66 if (abortControllerRef.current) {
67 abortControllerRef.current.abort();
68 }
69
70 // Create new abort controller
71 abortControllerRef.current = new AbortController();
72
73 setState(prev => ({ ...prev, loading: true, error: null }));
74
75 try {
76 const response = await fetch(url, {
77 ...fetchOptions,
78 signal: abortControllerRef.current.signal,
79 });
80
81 if (!response.ok) {
82 throw new Error(`HTTP error! status: ${response.status}`);
83 }
84
85 const data = await response.json();
86 const transformedData = transform(data);
87
88 // Update cache
89 if (useCache) {
90 cache.set(url, { data: transformedData, timestamp: Date.now() });
91 }
92
93 setState({ data: transformedData, error: null, loading: false });
94 onSuccess?.(transformedData);
95 } catch (error) {
96 if (error instanceof Error) {
97 if (error.name !== 'AbortError') {
98 setState({ data: null, error, loading: false });
99 onError?.(error);
100 }
101 }
102 }
103 }, [url, useCache, cacheTime, transform, onSuccess, onError, ...deps]);
104
105 const mutate = useCallback((newData: T) => {
106 setState(prev => ({ ...prev, data: newData }));
107 if (useCache && url) {
108 cache.set(url, { data: newData, timestamp: Date.now() });
109 }
110 }, [url, useCache]);
111
112 useEffect(() => {
113 fetchData();
114
115 return () => {
116 if (abortControllerRef.current) {
117 abortControllerRef.current.abort();
118 }
119 };
120 }, [fetchData]);
121
122 return {
123 ...state,
124 refetch: fetchData,
125 mutate,
126 };
127}
128
129// Usage example
130function UserProfile({ userId }: { userId: string }) {
131 const { data: user, error, loading, refetch, mutate } = useFetch<User>(
132 `/api/users/${userId}`,
133 {
134 deps: [userId],
135 onSuccess: (data) => console.log('User loaded:', data),
136 onError: (error) => console.error('Failed to load user:', error),
137 transform: (data) => ({
138 ...data,
139 fullName: `${data.firstName} ${data.lastName}`,
140 }),
141 }
142 );
143
144 const handleUpdate = async (updates: Partial<User>) => {
145 // Optimistic update
146 if (user) {
147 mutate({ ...user, ...updates });
148 }
149
150 try {
151 const response = await fetch(`/api/users/${userId}`, {
152 method: 'PATCH',
153 headers: { 'Content-Type': 'application/json' },
154 body: JSON.stringify(updates),
155 });
156
157 if (!response.ok) throw new Error('Update failed');
158
159 // Refetch to ensure consistency
160 await refetch();
161 } catch (error) {
162 // Revert on error
163 await refetch();
164 throw error;
165 }
166 };
167
168 if (loading) return <div>Loading...</div>;
169 if (error) return <div>Error: {error.message}</div>;
170 if (!user) return <div>No user found</div>;
171
172 return (
173 <div>
174 <h2>{user.fullName}</h2>
175 <button onClick={() => handleUpdate({ active: !user.active })}>
176 Toggle Active
177 </button>
178 <button onClick={refetch}>Refresh</button>
179 </div>
180 );
181}

useAsync

Execute async operations with proper state management

1// useAsync - Manage async operations elegantly
2import { useState, useCallback, useRef, useEffect } from 'react';
3
4interface AsyncState<T> {
5 data: T | null;
6 error: Error | null;
7 loading: boolean;
8 isIdle: boolean;
9 isLoading: boolean;
10 isError: boolean;
11 isSuccess: boolean;
12}
13
14function useAsync<T>() {
15 const [state, setState] = useState<AsyncState<T>>({
16 data: null,
17 error: null,
18 loading: false,
19 isIdle: true,
20 isLoading: false,
21 isError: false,
22 isSuccess: false,
23 });
24
25 const isMountedRef = useRef(true);
26
27 useEffect(() => {
28 return () => {
29 isMountedRef.current = false;
30 };
31 }, []);
32
33 const execute = useCallback(async (asyncFunction: () => Promise<T>) => {
34 setState({
35 data: null,
36 error: null,
37 loading: true,
38 isIdle: false,
39 isLoading: true,
40 isError: false,
41 isSuccess: false,
42 });
43
44 try {
45 const data = await asyncFunction();
46
47 if (isMountedRef.current) {
48 setState({
49 data,
50 error: null,
51 loading: false,
52 isIdle: false,
53 isLoading: false,
54 isError: false,
55 isSuccess: true,
56 });
57 }
58
59 return data;
60 } catch (error) {
61 if (isMountedRef.current) {
62 setState({
63 data: null,
64 error: error instanceof Error ? error : new Error(String(error)),
65 loading: false,
66 isIdle: false,
67 isLoading: false,
68 isError: true,
69 isSuccess: false,
70 });
71 }
72 throw error;
73 }
74 }, []);
75
76 const reset = useCallback(() => {
77 setState({
78 data: null,
79 error: null,
80 loading: false,
81 isIdle: true,
82 isLoading: false,
83 isError: false,
84 isSuccess: false,
85 });
86 }, []);
87
88 return { ...state, execute, reset };
89}
90
91// Advanced version with automatic execution
92function useAsyncEffect<T>(
93 asyncFunction: () => Promise<T>,
94 deps: React.DependencyList = []
95) {
96 const { execute, ...state } = useAsync<T>();
97
98 useEffect(() => {
99 execute(asyncFunction);
100 }, deps);
101
102 return state;
103}
104
105// Usage example
106function FileUploader() {
107 const {
108 data,
109 loading,
110 error,
111 isSuccess,
112 execute,
113 reset
114 } = useAsync<{ url: string; id: string }>();
115
116 const handleUpload = async (file: File) => {
117 const formData = new FormData();
118 formData.append('file', file);
119
120 try {
121 const result = await execute(async () => {
122 const response = await fetch('/api/upload', {
123 method: 'POST',
124 body: formData,
125 });
126
127 if (!response.ok) {
128 throw new Error('Upload failed');
129 }
130
131 return response.json();
132 });
133
134 console.log('Upload successful:', result);
135 } catch (error) {
136 console.error('Upload error:', error);
137 }
138 };
139
140 return (
141 <div>
142 <input
143 type="file"
144 onChange={(e) => {
145 const file = e.target.files?.[0];
146 if (file) handleUpload(file);
147 }}
148 disabled={loading}
149 />
150
151 {loading && <div>Uploading...</div>}
152
153 {isSuccess && data && (
154 <div>
155 <p>Upload complete!</p>
156 <a href={data.url}>View file</a>
157 <button onClick={reset}>Upload another</button>
158 </div>
159 )}
160
161 {error && (
162 <div>
163 <p>Error: {error.message}</p>
164 <button onClick={reset}>Try again</button>
165 </div>
166 )}
167 </div>
168 );
169}
170
171// Auto-retry example
172function DataLoaderWithRetry() {
173 const [retryCount, setRetryCount] = useState(0);
174 const maxRetries = 3;
175
176 const { data, error, loading, execute } = useAsync<any>();
177
178 const loadData = useCallback(async () => {
179 try {
180 await execute(async () => {
181 const response = await fetch('/api/data');
182 if (!response.ok) throw new Error('Failed to load');
183 return response.json();
184 });
185 setRetryCount(0);
186 } catch (err) {
187 if (retryCount < maxRetries) {
188 setRetryCount(prev => prev + 1);
189 setTimeout(() => loadData(), 1000 * Math.pow(2, retryCount));
190 }
191 }
192 }, [execute, retryCount]);
193
194 useEffect(() => {
195 loadData();
196 }, []);
197
198 return (
199 <div>
200 {loading && <div>Loading... {retryCount > 0 && `(Retry ${retryCount}/${maxRetries})`}</div>}
201 {error && retryCount >= maxRetries && <div>Failed after {maxRetries} retries</div>}
202 {data && <div>Data loaded successfully</div>}
203 </div>
204 );
205}

useInfiniteScroll

Implement infinite scrolling with pagination

1// useInfiniteScroll - Paginated data loading
2import { useState, useEffect, useCallback, useRef } from 'react';
3
4interface UseInfiniteScrollOptions {
5 threshold?: number;
6 rootMargin?: string;
7}
8
9interface UseInfiniteScrollReturn<T> {
10 items: T[];
11 loading: boolean;
12 error: Error | null;
13 hasMore: boolean;
14 loadMore: () => void;
15 reset: () => void;
16 observerTarget: (node: HTMLElement | null) => void;
17}
18
19function useInfiniteScroll<T>(
20 fetchFn: (page: number) => Promise<{ items: T[]; hasMore: boolean }>,
21 options: UseInfiniteScrollOptions = {}
22): UseInfiniteScrollReturn<T> {
23 const { threshold = 0.1, rootMargin = '100px' } = options;
24
25 const [items, setItems] = useState<T[]>([]);
26 const [page, setPage] = useState(1);
27 const [loading, setLoading] = useState(false);
28 const [error, setError] = useState<Error | null>(null);
29 const [hasMore, setHasMore] = useState(true);
30
31 const observer = useRef<IntersectionObserver>();
32 const loadingRef = useRef(false);
33
34 const loadMore = useCallback(async () => {
35 if (loadingRef.current || !hasMore) return;
36
37 loadingRef.current = true;
38 setLoading(true);
39 setError(null);
40
41 try {
42 const { items: newItems, hasMore: more } = await fetchFn(page);
43
44 setItems(prev => [...prev, ...newItems]);
45 setHasMore(more);
46 setPage(prev => prev + 1);
47 } catch (err) {
48 setError(err instanceof Error ? err : new Error('Failed to load'));
49 } finally {
50 setLoading(false);
51 loadingRef.current = false;
52 }
53 }, [fetchFn, page, hasMore]);
54
55 const observerTarget = useCallback((node: HTMLElement | null) => {
56 if (loading) return;
57
58 if (observer.current) observer.current.disconnect();
59
60 observer.current = new IntersectionObserver(
61 entries => {
62 if (entries[0].isIntersecting && hasMore && !loadingRef.current) {
63 loadMore();
64 }
65 },
66 { threshold, rootMargin }
67 );
68
69 if (node) observer.current.observe(node);
70 }, [loading, hasMore, loadMore, threshold, rootMargin]);
71
72 const reset = useCallback(() => {
73 setItems([]);
74 setPage(1);
75 setHasMore(true);
76 setError(null);
77 loadingRef.current = false;
78 }, []);
79
80 // Load initial data
81 useEffect(() => {
82 loadMore();
83 }, []);
84
85 return {
86 items,
87 loading,
88 error,
89 hasMore,
90 loadMore,
91 reset,
92 observerTarget,
93 };
94}
95
96// Usage example
97function InfiniteList() {
98 const fetchPosts = async (page: number) => {
99 const response = await fetch(`/api/posts?page=${page}&limit=20`);
100 const data = await response.json();
101
102 return {
103 items: data.posts,
104 hasMore: data.hasMore,
105 };
106 };
107
108 const {
109 items,
110 loading,
111 error,
112 hasMore,
113 observerTarget,
114 reset,
115 } = useInfiniteScroll(fetchPosts);
116
117 return (
118 <div className="infinite-scroll-container">
119 <button onClick={reset}>Reset</button>
120
121 <div className="posts-grid">
122 {items.map((post, index) => (
123 <article key={`${post.id}-${index}`} className="post-card">
124 <h3>{post.title}</h3>
125 <p>{post.excerpt}</p>
126 </article>
127 ))}
128 </div>
129
130 {error && (
131 <div className="error">
132 Error loading posts: {error.message}
133 </div>
134 )}
135
136 <div ref={observerTarget} className="load-more-trigger">
137 {loading && <div className="spinner">Loading more...</div>}
138 {!hasMore && items.length > 0 && (
139 <div className="end-message">No more posts to load</div>
140 )}
141 </div>
142 </div>
143 );
144}
145
146// Virtual scrolling variant
147function useVirtualInfiniteScroll<T>(
148 fetchFn: (page: number) => Promise<{ items: T[]; hasMore: boolean }>,
149 itemHeight: number,
150 containerHeight: number
151) {
152 const { items, ...scrollProps } = useInfiniteScroll(fetchFn);
153 const [scrollTop, setScrollTop] = useState(0);
154
155 const visibleRange = useMemo(() => {
156 const start = Math.floor(scrollTop / itemHeight);
157 const end = Math.ceil((scrollTop + containerHeight) / itemHeight);
158 return { start, end };
159 }, [scrollTop, itemHeight, containerHeight]);
160
161 const visibleItems = useMemo(() => {
162 return items.slice(visibleRange.start, visibleRange.end);
163 }, [items, visibleRange]);
164
165 const totalHeight = items.length * itemHeight;
166 const offsetY = visibleRange.start * itemHeight;
167
168 return {
169 ...scrollProps,
170 items,
171 visibleItems,
172 totalHeight,
173 offsetY,
174 onScroll: (e: React.UIEvent<HTMLElement>) => {
175 setScrollTop(e.currentTarget.scrollTop);
176 },
177 };
178}

Animation, Performance, and Optimization Hooks

Animation & Performance Hooks

useAnimation

Declarative animations with requestAnimationFrame

1// useAnimation - Smooth animations with RAF
2import { useState, useEffect, useRef, useCallback } from 'react';
3
4interface AnimationOptions {
5 duration: number;
6 easing?: (t: number) => number;
7 onComplete?: () => void;
8}
9
10const easings = {
11 linear: (t: number) => t,
12 easeInQuad: (t: number) => t * t,
13 easeOutQuad: (t: number) => t * (2 - t),
14 easeInOutQuad: (t: number) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
15 easeInCubic: (t: number) => t * t * t,
16 easeOutCubic: (t: number) => (--t) * t * t + 1,
17 easeInOutCubic: (t: number) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
18};
19
20function useAnimation(
21 from: number,
22 to: number,
23 options: AnimationOptions
24): [number, () => void, () => void] {
25 const { duration, easing = easings.easeInOutQuad, onComplete } = options;
26 const [value, setValue] = useState(from);
27 const animationRef = useRef<number>();
28 const startTimeRef = useRef<number>();
29 const fromRef = useRef(from);
30 const toRef = useRef(to);
31
32 const animate = useCallback(() => {
33 const now = performance.now();
34 if (!startTimeRef.current) startTimeRef.current = now;
35
36 const elapsed = now - startTimeRef.current;
37 const progress = Math.min(elapsed / duration, 1);
38 const easedProgress = easing(progress);
39
40 const currentValue = fromRef.current + (toRef.current - fromRef.current) * easedProgress;
41 setValue(currentValue);
42
43 if (progress < 1) {
44 animationRef.current = requestAnimationFrame(animate);
45 } else {
46 onComplete?.();
47 }
48 }, [duration, easing, onComplete]);
49
50 const start = useCallback(() => {
51 startTimeRef.current = undefined;
52 fromRef.current = value;
53 toRef.current = to;
54 animate();
55 }, [animate, value, to]);
56
57 const stop = useCallback(() => {
58 if (animationRef.current) {
59 cancelAnimationFrame(animationRef.current);
60 }
61 }, []);
62
63 useEffect(() => {
64 return () => stop();
65 }, [stop]);
66
67 return [value, start, stop];
68}
69
70// Spring animation hook
71function useSpring(
72 target: number,
73 config: {
74 stiffness?: number;
75 damping?: number;
76 mass?: number;
77 velocity?: number;
78 } = {}
79) {
80 const {
81 stiffness = 170,
82 damping = 26,
83 mass = 1,
84 velocity: initialVelocity = 0,
85 } = config;
86
87 const [value, setValue] = useState(target);
88 const [velocity, setVelocity] = useState(initialVelocity);
89 const animationRef = useRef<number>();
90 const lastTimeRef = useRef<number>();
91
92 useEffect(() => {
93 let animating = true;
94
95 const animate = (time: number) => {
96 if (!lastTimeRef.current) lastTimeRef.current = time;
97
98 const deltaTime = Math.min((time - lastTimeRef.current) / 1000, 0.064);
99 lastTimeRef.current = time;
100
101 const spring = -stiffness * (value - target);
102 const damper = -damping * velocity;
103 const acceleration = (spring + damper) / mass;
104
105 const newVelocity = velocity + acceleration * deltaTime;
106 const newValue = value + newVelocity * deltaTime;
107
108 setValue(newValue);
109 setVelocity(newVelocity);
110
111 // Check if animation should continue
112 if (
113 Math.abs(newVelocity) > 0.01 ||
114 Math.abs(newValue - target) > 0.01
115 ) {
116 if (animating) {
117 animationRef.current = requestAnimationFrame(animate);
118 }
119 } else {
120 setValue(target);
121 setVelocity(0);
122 }
123 };
124
125 animationRef.current = requestAnimationFrame(animate);
126
127 return () => {
128 animating = false;
129 if (animationRef.current) {
130 cancelAnimationFrame(animationRef.current);
131 }
132 };
133 }, [target, stiffness, damping, mass, value, velocity]);
134
135 return value;
136}
137
138// Usage example
139function AnimatedCounter() {
140 const [target, setTarget] = useState(0);
141 const [count, startAnimation] = useAnimation(0, target, {
142 duration: 1000,
143 easing: easings.easeOutCubic,
144 onComplete: () => console.log('Animation complete!'),
145 });
146
147 return (
148 <div>
149 <h2>{Math.round(count)}</h2>
150 <button onClick={() => {
151 setTarget(target + 100);
152 startAnimation();
153 }}>
154 Increment by 100
155 </button>
156 </div>
157 );
158}
159
160// Spring animation example
161function SpringBox() {
162 const [isOpen, setIsOpen] = useState(false);
163 const width = useSpring(isOpen ? 300 : 100, {
164 stiffness: 300,
165 damping: 30,
166 });
167 const height = useSpring(isOpen ? 200 : 100, {
168 stiffness: 300,
169 damping: 30,
170 });
171
172 return (
173 <div
174 onClick={() => setIsOpen(!isOpen)}
175 style={{
176 width: `${width}px`,
177 height: `${height}px`,
178 backgroundColor: 'purple',
179 cursor: 'pointer',
180 borderRadius: '8px',
181 }}
182 >
183 Click to toggle
184 </div>
185 );
186}

useThrottle

Throttle function calls and values

1// useThrottle - Limit execution frequency
2import { useState, useEffect, useRef, useCallback } from 'react';
3
4function useThrottle<T>(value: T, limit: number): T {
5 const [throttledValue, setThrottledValue] = useState<T>(value);
6 const lastRun = useRef(Date.now());
7
8 useEffect(() => {
9 const handler = setTimeout(() => {
10 if (Date.now() - lastRun.current >= limit) {
11 setThrottledValue(value);
12 lastRun.current = Date.now();
13 }
14 }, limit - (Date.now() - lastRun.current));
15
16 return () => clearTimeout(handler);
17 }, [value, limit]);
18
19 return throttledValue;
20}
21
22// Throttled callback hook
23function useThrottledCallback<T extends (...args: any[]) => any>(
24 callback: T,
25 delay: number,
26 deps: React.DependencyList = []
27): T {
28 const lastRun = useRef(Date.now());
29 const timeoutRef = useRef<NodeJS.Timeout>();
30
31 const throttledCallback = useCallback((...args: Parameters<T>) => {
32 const now = Date.now();
33 const timeSinceLastRun = now - lastRun.current;
34
35 if (timeSinceLastRun >= delay) {
36 callback(...args);
37 lastRun.current = now;
38 } else {
39 clearTimeout(timeoutRef.current);
40 timeoutRef.current = setTimeout(() => {
41 callback(...args);
42 lastRun.current = Date.now();
43 }, delay - timeSinceLastRun);
44 }
45 }, [callback, delay, ...deps]) as T;
46
47 useEffect(() => {
48 return () => {
49 if (timeoutRef.current) clearTimeout(timeoutRef.current);
50 };
51 }, []);
52
53 return throttledCallback;
54}
55
56// Leading/trailing throttle options
57function useAdvancedThrottle<T extends (...args: any[]) => any>(
58 callback: T,
59 delay: number,
60 options: { leading?: boolean; trailing?: boolean } = {}
61): T {
62 const { leading = true, trailing = true } = options;
63 const lastArgs = useRef<Parameters<T>>();
64 const lastThis = useRef<any>();
65 const timerId = useRef<NodeJS.Timeout>();
66 const lastCallTime = useRef<number>();
67 const lastInvokeTime = useRef(0);
68
69 const invokeFunc = useCallback((time: number) => {
70 const args = lastArgs.current!;
71 const thisArg = lastThis.current;
72
73 lastArgs.current = lastThis.current = undefined;
74 lastInvokeTime.current = time;
75 callback.apply(thisArg, args);
76 }, [callback]);
77
78 const leadingEdge = useCallback((time: number) => {
79 lastInvokeTime.current = time;
80 timerId.current = setTimeout(() => {
81 if (trailing && lastArgs.current) {
82 invokeFunc(Date.now());
83 }
84 timerId.current = undefined;
85 }, delay);
86
87 if (leading) {
88 invokeFunc(time);
89 }
90 }, [delay, leading, trailing, invokeFunc]);
91
92 const throttled = useCallback(function(this: any, ...args: Parameters<T>) {
93 const time = Date.now();
94 const isInvoking = shouldInvoke(time);
95
96 lastArgs.current = args;
97 lastThis.current = this;
98 lastCallTime.current = time;
99
100 if (isInvoking) {
101 if (!timerId.current) {
102 leadingEdge(lastCallTime.current);
103 }
104 }
105 }, [leadingEdge]) as T;
106
107 const shouldInvoke = (time: number) => {
108 const timeSinceLastCall = time - (lastCallTime.current || 0);
109 const timeSinceLastInvoke = time - lastInvokeTime.current;
110
111 return (
112 !lastCallTime.current ||
113 timeSinceLastCall >= delay ||
114 timeSinceLastInvoke >= delay
115 );
116 };
117
118 useEffect(() => {
119 return () => {
120 if (timerId.current) clearTimeout(timerId.current);
121 };
122 }, []);
123
124 return throttled;
125}
126
127// Usage example
128function ScrollTracker() {
129 const [scrollY, setScrollY] = useState(0);
130 const throttledScrollY = useThrottle(scrollY, 100);
131
132 const handleScroll = useThrottledCallback(() => {
133 console.log('Throttled scroll event');
134 // Heavy computation here
135 }, 200);
136
137 useEffect(() => {
138 const onScroll = () => {
139 setScrollY(window.scrollY);
140 handleScroll();
141 };
142
143 window.addEventListener('scroll', onScroll);
144 return () => window.removeEventListener('scroll', onScroll);
145 }, [handleScroll]);
146
147 return (
148 <div>
149 <p>Current scroll: {scrollY}px</p>
150 <p>Throttled scroll: {throttledScrollY}px</p>
151 </div>
152 );
153}

useRenderCount

Track component render count for optimization

1// useRenderCount - Debug render performance
2import { useRef, useEffect } from 'react';
3
4function useRenderCount(componentName?: string) {
5 const renderCount = useRef(0);
6 renderCount.current += 1;
7
8 useEffect(() => {
9 if (componentName) {
10 console.log(`${componentName} rendered ${renderCount.current} times`);
11 }
12 });
13
14 return renderCount.current;
15}
16
17// Why did you render hook
18function useWhyDidYouUpdate<T extends Record<string, any>>(
19 name: string,
20 props: T
21) {
22 const previousProps = useRef<T>();
23
24 useEffect(() => {
25 if (previousProps.current) {
26 const allKeys = Object.keys({ ...previousProps.current, ...props });
27 const changedProps: Record<string, any> = {};
28
29 allKeys.forEach(key => {
30 if (previousProps.current![key] !== props[key]) {
31 changedProps[key] = {
32 from: previousProps.current![key],
33 to: props[key]
34 };
35 }
36 });
37
38 if (Object.keys(changedProps).length) {
39 console.log('[why-did-you-update]', name, changedProps);
40 }
41 }
42
43 previousProps.current = props;
44 });
45}
46
47// Performance monitoring hook
48function usePerformanceMonitor(componentName: string) {
49 const renderCount = useRef(0);
50 const renderTimes = useRef<number[]>([]);
51 const lastRenderTime = useRef<number>();
52
53 useEffect(() => {
54 const now = performance.now();
55
56 if (lastRenderTime.current) {
57 const timeSinceLastRender = now - lastRenderTime.current;
58 renderTimes.current.push(timeSinceLastRender);
59
60 // Keep only last 10 render times
61 if (renderTimes.current.length > 10) {
62 renderTimes.current.shift();
63 }
64
65 const avgRenderTime = renderTimes.current.reduce((a, b) => a + b, 0) / renderTimes.current.length;
66
67 if (avgRenderTime > 16.67) { // More than one frame
68 console.warn(`${componentName} avg render time: ${avgRenderTime.toFixed(2)}ms`);
69 }
70 }
71
72 lastRenderTime.current = now;
73 renderCount.current++;
74 });
75
76 return {
77 renderCount: renderCount.current,
78 avgRenderTime: renderTimes.current.length > 0
79 ? renderTimes.current.reduce((a, b) => a + b, 0) / renderTimes.current.length
80 : 0
81 };
82}
83
84// Usage example
85function ExpensiveComponent({ data, filter }: { data: any[]; filter: string }) {
86 const renderCount = useRenderCount('ExpensiveComponent');
87
88 useWhyDidYouUpdate('ExpensiveComponent', { data, filter });
89
90 const { avgRenderTime } = usePerformanceMonitor('ExpensiveComponent');
91
92 const filteredData = useMemo(() => {
93 return data.filter(item => item.name.includes(filter));
94 }, [data, filter]);
95
96 return (
97 <div>
98 <div className="debug-info">
99 Renders: {renderCount} | Avg time: {avgRenderTime.toFixed(2)}ms
100 </div>
101
102 {filteredData.map(item => (
103 <div key={item.id}>{item.name}</div>
104 ))}
105 </div>
106 );
107}
108
109// Component profiler wrapper
110function withProfiler<P extends object>(
111 Component: React.ComponentType<P>,
112 id: string
113) {
114 return React.memo((props: P) => {
115 const onRender = (
116 id: string,
117 phase: 'mount' | 'update',
118 actualDuration: number,
119 baseDuration: number,
120 startTime: number,
121 commitTime: number
122 ) => {
123 console.log(`Profiler [${id}]:`, {
124 phase,
125 actualDuration,
126 baseDuration,
127 startTime,
128 commitTime
129 });
130 };
131
132 return (
133 <React.Profiler id={id} onRender={onRender}>
134 <Component {...props} />
135 </React.Profiler>
136 );
137 });
138}

Advertisement Space - mid-hooks

Google AdSense: rectangle

Custom Hook Best Practices

Hook Rules & Guidelines

  • Always prefix custom hooks with "use"
  • Call hooks at the top level of functions
  • Never call hooks inside conditionals or loops
  • Keep hooks pure and side-effect free when possible
  • Document hook parameters and return values
  • Provide TypeScript types for better DX

Performance Considerations

  • Memoize expensive computations
  • Clean up side effects in useEffect
  • Use useCallback for stable function references
  • Avoid creating new objects/arrays in render
  • Debounce or throttle frequent updates
  • Profile hooks to identify bottlenecks

Testing Custom Hooks

  • Use @testing-library/react-hooks
  • Test hook behavior, not implementation
  • Mock external dependencies
  • Test error states and edge cases
  • Verify cleanup functions work correctly
  • Test hooks in isolation when possible

Testing Custom Hooks

1// Testing custom hooks with @testing-library/react-hooks
2import { renderHook, act } from '@testing-library/react-hooks';
3import { useLocalStorage } from './useLocalStorage';
4
5describe('useLocalStorage', () => {
6 beforeEach(() => {
7 window.localStorage.clear();
8 });
9
10 it('should initialize with default value', () => {
11 const { result } = renderHook(() =>
12 useLocalStorage('test-key', 'default')
13 );
14
15 expect(result.current[0]).toBe('default');
16 });
17
18 it('should persist value to localStorage', () => {
19 const { result } = renderHook(() =>
20 useLocalStorage('test-key', 'initial')
21 );
22
23 act(() => {
24 result.current[1]('updated');
25 });
26
27 expect(result.current[0]).toBe('updated');
28 expect(window.localStorage.getItem('test-key')).toBe('"updated"');
29 });
30
31 it('should sync across multiple hooks', () => {
32 const { result: hook1 } = renderHook(() =>
33 useLocalStorage('shared-key', 'initial')
34 );
35 const { result: hook2 } = renderHook(() =>
36 useLocalStorage('shared-key', 'initial')
37 );
38
39 act(() => {
40 hook1.current[1]('updated');
41 });
42
43 // Both hooks should have the updated value
44 expect(hook1.current[0]).toBe('updated');
45 expect(hook2.current[0]).toBe('updated');
46 });
47
48 it('should handle errors gracefully', () => {
49 // Mock localStorage to throw error
50 const mockSetItem = jest.fn(() => {
51 throw new Error('Storage full');
52 });
53 Object.defineProperty(window, 'localStorage', {
54 value: { setItem: mockSetItem, getItem: jest.fn() },
55 writable: true
56 });
57
58 const { result } = renderHook(() =>
59 useLocalStorage('test-key', 'default')
60 );
61
62 // Should not throw when setting value
63 expect(() => {
64 act(() => {
65 result.current[1]('new value');
66 });
67 }).not.toThrow();
68 });
69});

Advertisement Space - bottom-hooks

Google AdSense: horizontal

Related Resources

Build Your Own Hook Library

Start creating reusable custom hooks to solve your specific application needs.

Explore Component Patterns →