Master the art of creating reusable custom hooks, implementing advanced patterns, and building comprehensive hook libraries for enterprise React applications.
Custom hooks are React's most powerful abstraction mechanism, allowing you to extract component logic into reusable functions that can be shared across your application. They enable you to create clean, testable, and maintainable code by encapsulating complex stateful logic.
Custom hooks follow the same rules as built-in hooks but give you the freedom to combine multiple hooks into higher-level abstractions. They're the key to building scalable React applications where business logic is separated from UI concerns, making your codebase more modular and easier to maintain.
In this comprehensive lesson, you'll learn to create sophisticated custom hooks that handle real-world scenarios like data fetching, form management, local storage synchronization, and complex async operations with proper error handling and performance optimization.
Custom hooks are functions that start with "use" and can call other hooks. They provide a way to share stateful logic between components while keeping the state separate.
Basic Custom Hook Structure:
1function useCustomHook(initialValue) {
2 const [state, setState] = useState(initialValue);
3
4 const updateState = useCallback((newValue) => {
5 setState(newValue);
6 }, []);
7
8 return { state, updateState };
9}Advanced Custom Hook with Multiple Concerns:
1function useApiData(url, options = {}) {
2 const [data, setData] = useState(null);
3 const [loading, setLoading] = useState(false);
4 const [error, setError] = useState(null);
5 const [controller, setController] = useState(null);
6
7 const fetchData = useCallback(async (customUrl = url) => {
8 try {
9 // Cancel previous request if still pending
10 if (controller) {
11 controller.abort();
12 }
13
14 const newController = new AbortController();
15 setController(newController);
16 setLoading(true);
17 setError(null);
18
19 const response = await fetch(customUrl, {
20 ...options,
21 signal: newController.signal
22 });
23
24 if (!response.ok) {
25 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
26 }
27
28 const result = await response.json();
29 setData(result);
30 } catch (err) {
31 if (err.name !== 'AbortError') {
32 setError(err.message);
33 }
34 } finally {
35 setLoading(false);
36 setController(null);
37 }
38 }, [url, options]);
39
40 const refetch = useCallback(() => fetchData(), [fetchData]);
41
42 const reset = useCallback(() => {
43 setData(null);
44 setError(null);
45 setLoading(false);
46 if (controller) {
47 controller.abort();
48 setController(null);
49 }
50 }, [controller]);
51
52 // Auto-fetch on mount and URL changes
53 useEffect(() => {
54 if (url) {
55 fetchData();
56 }
57
58 // Cleanup on unmount
59 return () => {
60 if (controller) {
61 controller.abort();
62 }
63 };
64 }, [url]);
65
66 return {
67 data,
68 loading,
69 error,
70 refetch,
71 reset,
72 fetchData: (customUrl) => fetchData(customUrl)
73 };
74}Custom Hook Best Practices:
1. Single Responsibility:
Each hook should have a clear, focused purpose.
1// Good: Focused on form state
2function useFormState(initialValues) {
3 // Form logic only
4}
5
6// Avoid: Mixed concerns
7function useFormAndApiAndLocalStorage() {
8 // Too many responsibilities
9}2. Return Objects for Multiple Values:
1// Good: Object with named properties
2function useCounter(initial = 0) {
3 const [count, setCount] = useState(initial);
4
5 return {
6 count,
7 increment: () => setCount(c => c + 1),
8 decrement: () => setCount(c => c - 1),
9 reset: () => setCount(initial)
10 };
11}
12
13// Less flexible: Array return
14function useCounter(initial = 0) {
15 const [count, setCount] = useState(initial);
16 return [count, setCount]; // Hard to extend
17}3. Proper Dependency Management:
1function useDebounce(value, delay) {
2 const [debouncedValue, setDebouncedValue] = useState(value);
3
4 useEffect(() => {
5 const handler = setTimeout(() => {
6 setDebouncedValue(value);
7 }, delay);
8
9 return () => {
10 clearTimeout(handler);
11 };
12 }, [value, delay]); // Proper dependencies
13
14 return debouncedValue;
15}4. Error Boundaries and Graceful Failures:
1function useLocalStorage(key, initialValue) {
2 const [value, setValue] = useState(() => {
3 try {
4 const item = window.localStorage.getItem(key);
5 return item ? JSON.parse(item) : initialValue;
6 } catch (error) {
7 console.warn(`Error reading localStorage key "${key}":, error`);
8 return initialValue;
9 }
10 });
11
12 const setStoredValue = useCallback((valueToStore) => {
13 try {
14 setValue(valueToStore);
15 window.localStorage.setItem(key, JSON.stringify(valueToStore));
16 } catch (error) {
17 console.warn(`Error setting localStorage key "${key}":, error`);
18 }
19 }, [key]);
20
21 return [value, setStoredValue];
22}5. TypeScript Integration:
1interface UseApiOptions {
2 method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
3 headers?: Record<string, string>;
4 body?: any;
5 enabled?: boolean;
6}
7
8interface UseApiReturn<T> {
9 data: T | null;
10 loading: boolean;
11 error: string | null;
12 refetch: () => Promise<void>;
13 reset: () => void;
14}
15
16function useApi<T = any>(
17 url: string,
18 options: UseApiOptions = {}
19): UseApiReturn<T> {
20 // Implementation with proper typing
21}These patterns ensure your custom hooks are reliable, maintainable, and provide excellent developer experience.
Advanced custom hooks combine multiple built-in hooks and other custom hooks to create powerful abstractions that solve complex problems.
Hook Composition Pattern:
1// Base hooks
2function useToggle(initialValue = false) {
3 const [value, setValue] = useState(initialValue);
4
5 const toggle = useCallback(() => setValue(v => !v), []);
6 const setTrue = useCallback(() => setValue(true), []);
7 const setFalse = useCallback(() => setValue(false), []);
8
9 return { value, toggle, setTrue, setFalse };
10}
11
12function useClickOutside(ref, handler) {
13 useEffect(() => {
14 const handleClick = (event) => {
15 if (ref.current && !ref.current.contains(event.target)) {
16 handler();
17 }
18 };
19
20 document.addEventListener('mousedown', handleClick);
21 return () => document.removeEventListener('mousedown', handleClick);
22 }, [ref, handler]);
23}
24
25// Composed hook combining multiple hooks
26function useDropdown() {
27 const { value: isOpen, toggle, setFalse: close } = useToggle();
28 const dropdownRef = useRef(null);
29 const triggerRef = useRef(null);
30
31 useClickOutside(dropdownRef, close);
32
33 const handleKeyDown = useCallback((event) => {
34 if (event.key === 'Escape') {
35 close();
36 }
37 }, [close]);
38
39 useEffect(() => {
40 if (isOpen) {
41 document.addEventListener('keydown', handleKeyDown);
42 return () => document.removeEventListener('keydown', handleKeyDown);
43 }
44 }, [isOpen, handleKeyDown]);
45
46 return {
47 isOpen,
48 toggle,
49 close,
50 dropdownRef,
51 triggerRef,
52 dropdownProps: {
53 ref: dropdownRef,
54 'aria-hidden': !isOpen,
55 'aria-expanded': isOpen
56 },
57 triggerProps: {
58 ref: triggerRef,
59 'aria-expanded': isOpen,
60 onClick: toggle
61 }
62 };
63}Compound Hook Pattern:
1function useFormField(name, validators = []) {
2 const [value, setValue] = useState('');
3 const [touched, setTouched] = useState(false);
4 const [errors, setErrors] = useState([]);
5
6 const validate = useCallback(async (valueToValidate = value) => {
7 const validationErrors = [];
8
9 for (const validator of validators) {
10 try {
11 const result = await validator(valueToValidate);
12 if (result !== true) {
13 validationErrors.push(result);
14 }
15 } catch (error) {
16 validationErrors.push(error.message);
17 }
18 }
19
20 setErrors(validationErrors);
21 return validationErrors.length === 0;
22 }, [value, validators]);
23
24 const handleChange = useCallback((newValue) => {
25 setValue(newValue);
26 if (touched) {
27 validate(newValue);
28 }
29 }, [touched, validate]);
30
31 const handleBlur = useCallback(() => {
32 setTouched(true);
33 validate();
34 }, [validate]);
35
36 return {
37 name,
38 value,
39 errors,
40 touched,
41 isValid: errors.length === 0,
42 handleChange,
43 handleBlur,
44 validate,
45 reset: () => {
46 setValue('');
47 setTouched(false);
48 setErrors([]);
49 }
50 };
51}
52
53function useForm(initialValues = {}, globalValidators = []) {
54 const [fields, setFields] = useState({});
55 const [isSubmitting, setIsSubmitting] = useState(false);
56 const [submitError, setSubmitError] = useState(null);
57
58 const registerField = useCallback((name, validators = []) => {
59 const field = useFormField(name, validators);
60
61 useEffect(() => {
62 setFields(prev => ({ ...prev, [name]: field }));
63 return () => {
64 setFields(prev => {
65 const { [name]: removed, ...rest } = prev;
66 return rest;
67 });
68 };
69 }, [name, field]);
70
71 return field;
72 }, []);
73
74 const validateForm = useCallback(async () => {
75 const fieldValidations = await Promise.all(
76 Object.values(fields).map(field => field.validate())
77 );
78
79 const globalValidations = await Promise.all(
80 globalValidators.map(validator => validator(getValues()))
81 );
82
83 return fieldValidations.every(Boolean) && globalValidations.every(Boolean);
84 }, [fields, globalValidators]);
85
86 const getValues = useCallback(() => {
87 return Object.entries(fields).reduce((acc, [name, field]) => {
88 acc[name] = field.value;
89 return acc;
90 }, {});
91 }, [fields]);
92
93 const reset = useCallback(() => {
94 Object.values(fields).forEach(field => field.reset());
95 setIsSubmitting(false);
96 setSubmitError(null);
97 }, [fields]);
98
99 const handleSubmit = useCallback(async (onSubmit) => {
100 setIsSubmitting(true);
101 setSubmitError(null);
102
103 try {
104 const isValid = await validateForm();
105 if (isValid) {
106 await onSubmit(getValues());
107 }
108 } catch (error) {
109 setSubmitError(error.message);
110 } finally {
111 setIsSubmitting(false);
112 }
113 }, [validateForm, getValues]);
114
115 return {
116 fields,
117 registerField,
118 getValues,
119 validateForm,
120 handleSubmit,
121 reset,
122 isSubmitting,
123 submitError,
124 isValid: Object.values(fields).every(field => field.isValid)
125 };
126}State Machine Hook Pattern:
1function useStateMachine(initialState, transitions) {
2 const [state, setState] = useState(initialState);
3 const [history, setHistory] = useState([initialState]);
4
5 const transition = useCallback((action) => {
6 const currentTransitions = transitions[state];
7 if (!currentTransitions || !currentTransitions[action]) {
8 console.warn(`Invalid transition: ${action} from state ${state}`);
9 return;
10 }
11
12 const nextState = currentTransitions[action];
13 setState(nextState);
14 setHistory(prev => [...prev, nextState]);
15 }, [state, transitions]);
16
17 const canTransition = useCallback((action) => {
18 const currentTransitions = transitions[state];
19 return currentTransitions && currentTransitions[action] !== undefined;
20 }, [state, transitions]);
21
22 const reset = useCallback(() => {
23 setState(initialState);
24 setHistory([initialState]);
25 }, [initialState]);
26
27 return {
28 state,
29 transition,
30 canTransition,
31 history,
32 reset
33 };
34}
35
36// Usage example
37const authStates = {
38 idle: { LOGIN: 'loading', LOGOUT: 'idle' },
39 loading: { SUCCESS: 'authenticated', ERROR: 'error' },
40 authenticated: { LOGOUT: 'idle', REFRESH: 'loading' },
41 error: { RETRY: 'loading', RESET: 'idle' }
42};
43
44function useAuth() {
45 const { state, transition, canTransition } = useStateMachine('idle', authStates);
46
47 const login = useCallback(async (credentials) => {
48 if (!canTransition('LOGIN')) return;
49
50 transition('LOGIN');
51 try {
52 await authApi.login(credentials);
53 transition('SUCCESS');
54 } catch (error) {
55 transition('ERROR');
56 throw error;
57 }
58 }, [transition, canTransition]);
59
60 return { state, login, canTransition };
61}Resource Management Hook:
1function useResource(factory, dependencies = []) {
2 const [resource, setResource] = useState(null);
3 const [loading, setLoading] = useState(true);
4 const [error, setError] = useState(null);
5
6 useEffect(() => {
7 let mounted = true;
8 let cleanup = null;
9
10 const createResource = async () => {
11 try {
12 setLoading(true);
13 setError(null);
14
15 const newResource = await factory();
16
17 if (mounted) {
18 setResource(newResource);
19
20 // Store cleanup function if resource provides one
21 if (newResource && typeof newResource.destroy === 'function') {
22 cleanup = () => newResource.destroy();
23 }
24 }
25 } catch (err) {
26 if (mounted) {
27 setError(err);
28 }
29 } finally {
30 if (mounted) {
31 setLoading(false);
32 }
33 }
34 };
35
36 createResource();
37
38 return () => {
39 mounted = false;
40 if (cleanup) {
41 cleanup();
42 }
43 };
44 }, dependencies);
45
46 return { resource, loading, error };
47}These advanced patterns enable you to build powerful, reusable hooks that handle complex scenarios while maintaining clean separation of concerns.
Creating a production-ready hook library requires careful consideration of API design, performance, testing, and documentation.
Hook Library Structure:
1src/
2 hooks/
3 core/
4 useToggle.ts
5 useCounter.ts
6 useLocalStorage.ts
7 async/
8 useApi.ts
9 useAsyncOperation.ts
10 useWebSocket.ts
11 forms/
12 useForm.ts
13 useFormField.ts
14 useValidation.ts
15 ui/
16 useDropdown.ts
17 useModal.ts
18 useTabs.ts
19 utils/
20 useDebounce.ts
21 useThrottle.ts
22 useClickOutside.ts
23 index.tsComprehensive Hook with Full TypeScript Support:
1interface UseAsyncOptions<T> {
2 immediate?: boolean;
3 initialData?: T;
4 onSuccess?: (data: T) => void;
5 onError?: (error: Error) => void;
6 retryCount?: number;
7 retryDelay?: number;
8}
9
10interface UseAsyncReturn<T, Args extends any[]> {
11 data: T | null;
12 loading: boolean;
13 error: Error | null;
14 execute: (...args: Args) => Promise<T>;
15 reset: () => void;
16 retry: () => Promise<T>;
17 cancel: () => void;
18}
19
20function useAsync<T, Args extends any[] = []>(
21 asyncFunction: (...args: Args) => Promise<T>,
22 options: UseAsyncOptions<T> = {}
23): UseAsyncReturn<T, Args> {
24 const {
25 immediate = false,
26 initialData = null,
27 onSuccess,
28 onError,
29 retryCount = 0,
30 retryDelay = 1000
31 } = options;
32
33 const [state, setState] = useState({
34 data: initialData,
35 loading: immediate,
36 error: null as Error | null
37 });
38
39 const [retryAttempts, setRetryAttempts] = useState(0);
40 const lastArgsRef = useRef<Args | null>(null);
41 const cancelTokenRef = useRef<{ cancelled: boolean }>({ cancelled: false });
42
43 const execute = useCallback(async (...args: Args): Promise<T> => {
44 const cancelToken = { cancelled: false };
45 cancelTokenRef.current = cancelToken;
46 lastArgsRef.current = args;
47
48 setState(prev => ({ ...prev, loading: true, error: null }));
49
50 try {
51 const result = await asyncFunction(...args);
52
53 if (!cancelToken.cancelled) {
54 setState({ data: result, loading: false, error: null });
55 setRetryAttempts(0);
56 onSuccess?.(result);
57 }
58
59 return result;
60 } catch (error) {
61 if (!cancelToken.cancelled) {
62 setState(prev => ({ ...prev, loading: false, error: error as Error }));
63 onError?.(error as Error);
64 }
65 throw error;
66 }
67 }, [asyncFunction, onSuccess, onError]);
68
69 const retry = useCallback(async (): Promise<T> => {
70 if (!lastArgsRef.current) {
71 throw new Error('No previous execution to retry');
72 }
73
74 if (retryAttempts < retryCount) {
75 setRetryAttempts(prev => prev + 1);
76
77 if (retryDelay > 0) {
78 await new Promise(resolve => setTimeout(resolve, retryDelay));
79 }
80
81 return execute(...lastArgsRef.current);
82 }
83
84 throw new Error('Maximum retry attempts reached');
85 }, [execute, retryAttempts, retryCount, retryDelay]);
86
87 const reset = useCallback(() => {
88 cancelTokenRef.current.cancelled = true;
89 setState({ data: initialData, loading: false, error: null });
90 setRetryAttempts(0);
91 lastArgsRef.current = null;
92 }, [initialData]);
93
94 const cancel = useCallback(() => {
95 cancelTokenRef.current.cancelled = true;
96 setState(prev => ({ ...prev, loading: false }));
97 }, []);
98
99 // Auto-execute if immediate is true
100 useEffect(() => {
101 if (immediate && lastArgsRef.current === null) {
102 execute();
103 }
104 }, [immediate, execute]);
105
106 // Cleanup on unmount
107 useEffect(() => {
108 return () => {
109 cancelTokenRef.current.cancelled = true;
110 };
111 }, []);
112
113 return {
114 ...state,
115 execute,
116 reset,
117 retry,
118 cancel
119 };
120}Hook Testing Strategy:
1import { renderHook, act } from '@testing-library/react';
2import { useAsync } from '../useAsync';
3
4describe('useAsync', () => {
5 it('should handle successful async operations', async () => {
6 const mockFn = jest.fn().mockResolvedValue('success');
7 const { result } = renderHook(() => useAsync(mockFn));
8
9 expect(result.current.loading).toBe(false);
10 expect(result.current.data).toBe(null);
11
12 await act(async () => {
13 await result.current.execute();
14 });
15
16 expect(result.current.loading).toBe(false);
17 expect(result.current.data).toBe('success');
18 expect(result.current.error).toBe(null);
19 });
20
21 it('should handle errors gracefully', async () => {
22 const error = new Error('Test error');
23 const mockFn = jest.fn().mockRejectedValue(error);
24 const { result } = renderHook(() => useAsync(mockFn));
25
26 await act(async () => {
27 try {
28 await result.current.execute();
29 } catch (e) {
30 // Expected to throw
31 }
32 });
33
34 expect(result.current.loading).toBe(false);
35 expect(result.current.data).toBe(null);
36 expect(result.current.error).toBe(error);
37 });
38
39 it('should support retry functionality', async () => {
40 let callCount = 0;
41 const mockFn = jest.fn().mockImplementation(() => {
42 callCount++;
43 if (callCount < 3) {
44 return Promise.reject(new Error('Retry test'));
45 }
46 return Promise.resolve('success after retry');
47 });
48
49 const { result } = renderHook(() =>
50 useAsync(mockFn, { retryCount: 3, retryDelay: 0 })
51 );
52
53 // First execution fails
54 await act(async () => {
55 try {
56 await result.current.execute();
57 } catch (e) {
58 // Expected to throw
59 }
60 });
61
62 expect(result.current.error).toBeTruthy();
63
64 // Retry succeeds
65 await act(async () => {
66 await result.current.retry();
67 });
68
69 expect(result.current.data).toBe('success after retry');
70 expect(result.current.error).toBe(null);
71 });
72});Hook Documentation Template:
1/**
2 * Custom hook for managing asynchronous operations with loading states,
3 * error handling, and retry functionality.
4 *
5 * @example
6 * ```tsx
7 * function UserProfile({ userId }: { userId: string }) {
8 * const { data: user, loading, error, execute } = useAsync(
9 * (id: string) => fetchUser(id),
10 * { immediate: true }
11 * );
12 *
13 * useEffect(() => {
14 * execute(userId);
15 * }, [userId, execute]);
16 *
17 * if (loading) return <Spinner />;
18 * if (error) return <Error message={error.message} />;
19 * if (!user) return <NotFound />;
20 *
21 * return <div>{user.name}</div>;
22 * }
23 * ```
24 *
25 * @param asyncFunction - The async function to execute
26 * @param options - Configuration options
27 * @returns Object containing state and control functions
28 */Performance Optimization Patterns:
1// Memoization for expensive computations
2function useExpensiveComputation<T, Args extends any[]>(
3 computeFn: (...args: Args) => T,
4 dependencies: Args
5): T {
6 return useMemo(() => computeFn(...dependencies), dependencies);
7}
8
9// Debounced hook for performance
10function useDebouncedCallback<T extends (...args: any[]) => any>(
11 callback: T,
12 delay: number,
13 dependencies: React.DependencyList
14): T {
15 const debouncedCallback = useMemo(() => {
16 return debounce(callback, delay);
17 }, [callback, delay, ...dependencies]);
18
19 return debouncedCallback as T;
20}
21
22// Virtualization hook for large lists
23function useVirtualList<T>(
24 items: T[],
25 options: {
26 itemHeight: number;
27 containerHeight: number;
28 overscan?: number;
29 }
30) {
31 const [scrollTop, setScrollTop] = useState(0);
32 const { itemHeight, containerHeight, overscan = 5 } = options;
33
34 const visibleItems = useMemo(() => {
35 const startIndex = Math.floor(scrollTop / itemHeight);
36 const endIndex = Math.min(
37 startIndex + Math.ceil(containerHeight / itemHeight) + overscan,
38 items.length - 1
39 );
40
41 return {
42 startIndex: Math.max(0, startIndex - overscan),
43 endIndex,
44 items: items.slice(
45 Math.max(0, startIndex - overscan),
46 endIndex + 1
47 )
48 };
49 }, [items, scrollTop, itemHeight, containerHeight, overscan]);
50
51 return {
52 ...visibleItems,
53 onScroll: (e: React.UIEvent<HTMLDivElement>) => {
54 setScrollTop(e.currentTarget.scrollTop);
55 },
56 totalHeight: items.length * itemHeight
57 };
58}Building production-ready hook libraries requires attention to performance, testing, documentation, and API design. These patterns ensure your hooks are reliable, maintainable, and provide excellent developer experience in large-scale applications.
A comprehensive demonstration of custom hook development patterns including async operations, form management, local storage, and compound hook architectures.
1import React, { useState, useEffect, useCallback, useMemo, useRef, useContext, createContext } from 'react';
2
3// =====================================================================
4// Core Utility Hooks
5// =====================================================================
6
7function useToggle(initialValue = false) {
8 const [value, setValue] = useState(initialValue);
9
10 const toggle = useCallback(() => setValue(v => !v), []);
11 const setTrue = useCallback(() => setValue(true), []);
12 const setFalse = useCallback(() => setValue(false), []);
13
14 return { value, toggle, setTrue, setFalse };
15}
16
17function useDebounce(value, delay) {
18 const [debouncedValue, setDebouncedValue] = useState(value);
19
20 useEffect(() => {
21 const handler = setTimeout(() => {
22 setDebouncedValue(value);
23 }, delay);
24
25 return () => {
26 clearTimeout(handler);
27 };
28 }, [value, delay]);
29
30 return debouncedValue;
31}
32
33function useLocalStorage(key, initialValue) {
34 const [storedValue, setStoredValue] = useState(() => {
35 try {
36 const item = window.localStorage.getItem(key);
37 return item ? JSON.parse(item) : initialValue;
38 } catch (error) {
39 console.warn(`Error reading localStorage key "${key}":, error`);
40 return initialValue;
41 }
42 });
43
44 const setValue = useCallback((value) => {
45 try {
46 setStoredValue(value);
47 window.localStorage.setItem(key, JSON.stringify(value));
48 } catch (error) {
49 console.warn(`Error setting localStorage key "${key}":, error`);
50 }
51 }, [key]);
52
53 return [storedValue, setValue];
54}
55
56function useClickOutside(ref, handler) {
57 useEffect(() => {
58 const handleClick = (event) => {
59 if (ref.current && !ref.current.contains(event.target)) {
60 handler();
61 }
62 };
63
64 document.addEventListener('mousedown', handleClick);
65 return () => document.removeEventListener('mousedown', handleClick);
66 }, [ref, handler]);
67}
68
69// =====================================================================
70// Advanced Async Hook
71// =====================================================================
72
73function useAsync(asyncFunction, options = {}) {
74 const {
75 immediate = false,
76 initialData = null,
77 onSuccess,
78 onError,
79 retryCount = 0,
80 retryDelay = 1000
81 } = options;
82
83 const [state, setState] = useState({
84 data: initialData,
85 loading: immediate,
86 error: null
87 });
88
89 const [retryAttempts, setRetryAttempts] = useState(0);
90 const lastArgsRef = useRef(null);
91 const cancelTokenRef = useRef({ cancelled: false });
92
93 const execute = useCallback(async (...args) => {
94 const cancelToken = { cancelled: false };
95 cancelTokenRef.current = cancelToken;
96 lastArgsRef.current = args;
97
98 setState(prev => ({ ...prev, loading: true, error: null }));
99
100 try {
101 const result = await asyncFunction(...args);
102
103 if (!cancelToken.cancelled) {
104 setState({ data: result, loading: false, error: null });
105 setRetryAttempts(0);
106 onSuccess?.(result);
107 }
108
109 return result;
110 } catch (error) {
111 if (!cancelToken.cancelled) {
112 setState(prev => ({ ...prev, loading: false, error }));
113 onError?.(error);
114 }
115 throw error;
116 }
117 }, [asyncFunction, onSuccess, onError]);
118
119 const retry = useCallback(async () => {
120 if (!lastArgsRef.current) {
121 throw new Error('No previous execution to retry');
122 }
123
124 if (retryAttempts < retryCount) {
125 setRetryAttempts(prev => prev + 1);
126
127 if (retryDelay > 0) {
128 await new Promise(resolve => setTimeout(resolve, retryDelay));
129 }
130
131 return execute(...lastArgsRef.current);
132 }
133
134 throw new Error('Maximum retry attempts reached');
135 }, [execute, retryAttempts, retryCount, retryDelay]);
136
137 const reset = useCallback(() => {
138 cancelTokenRef.current.cancelled = true;
139 setState({ data: initialData, loading: false, error: null });
140 setRetryAttempts(0);
141 lastArgsRef.current = null;
142 }, [initialData]);
143
144 const cancel = useCallback(() => {
145 cancelTokenRef.current.cancelled = true;
146 setState(prev => ({ ...prev, loading: false }));
147 }, []);
148
149 // Auto-execute if immediate is true
150 useEffect(() => {
151 if (immediate && lastArgsRef.current === null) {
152 execute();
153 }
154 }, [immediate, execute]);
155
156 // Cleanup on unmount
157 useEffect(() => {
158 return () => {
159 cancelTokenRef.current.cancelled = true;
160 };
161 }, []);
162
163 return {
164 ...state,
165 execute,
166 reset,
167 retry,
168 cancel
169 };
170}
171
172// =====================================================================
173// Advanced Form Management Hook
174// =====================================================================
175
176function useFormField(name, validators = []) {
177 const [value, setValue] = useState('');
178 const [touched, setTouched] = useState(false);
179 const [errors, setErrors] = useState([]);
180
181 const validate = useCallback(async (valueToValidate = value) => {
182 const validationErrors = [];
183
184 for (const validator of validators) {
185 try {
186 const result = await validator(valueToValidate);
187 if (result !== true) {
188 validationErrors.push(result);
189 }
190 } catch (error) {
191 validationErrors.push(error.message);
192 }
193 }
194
195 setErrors(validationErrors);
196 return validationErrors.length === 0;
197 }, [value, validators]);
198
199 const handleChange = useCallback((newValue) => {
200 setValue(newValue);
201 if (touched) {
202 validate(newValue);
203 }
204 }, [touched, validate]);
205
206 const handleBlur = useCallback(() => {
207 setTouched(true);
208 validate();
209 }, [validate]);
210
211 return {
212 name,
213 value,
214 errors,
215 touched,
216 isValid: errors.length === 0,
217 handleChange,
218 handleBlur,
219 validate,
220 reset: () => {
221 setValue('');
222 setTouched(false);
223 setErrors([]);
224 }
225 };
226}
227
228function useForm(initialValues = {}, globalValidators = []) {
229 const [fields, setFields] = useState({});
230 const [isSubmitting, setIsSubmitting] = useState(false);
231 const [submitError, setSubmitError] = useState(null);
232
233 const registerField = useCallback((name, validators = []) => {
234 const field = useFormField(name, validators);
235
236 useEffect(() => {
237 setFields(prev => ({ ...prev, [name]: field }));
238 return () => {
239 setFields(prev => {
240 const { [name]: removed, ...rest } = prev;
241 return rest;
242 });
243 };
244 }, [name, field]);
245
246 return field;
247 }, []);
248
249 const validateForm = useCallback(async () => {
250 const fieldValidations = await Promise.all(
251 Object.values(fields).map(field => field.validate())
252 );
253
254 const globalValidations = await Promise.all(
255 globalValidators.map(validator => validator(getValues()))
256 );
257
258 return fieldValidations.every(Boolean) && globalValidations.every(Boolean);
259 }, [fields, globalValidators]);
260
261 const getValues = useCallback(() => {
262 return Object.entries(fields).reduce((acc, [name, field]) => {
263 acc[name] = field.value;
264 return acc;
265 }, {});
266 }, [fields]);
267
268 const reset = useCallback(() => {
269 Object.values(fields).forEach(field => field.reset());
270 setIsSubmitting(false);
271 setSubmitError(null);
272 }, [fields]);
273
274 const handleSubmit = useCallback(async (onSubmit) => {
275 setIsSubmitting(true);
276 setSubmitError(null);
277
278 try {
279 const isValid = await validateForm();
280 if (isValid) {
281 await onSubmit(getValues());
282 }
283 } catch (error) {
284 setSubmitError(error.message);
285 } finally {
286 setIsSubmitting(false);
287 }
288 }, [validateForm, getValues]);
289
290 return {
291 fields,
292 registerField,
293 getValues,
294 validateForm,
295 handleSubmit,
296 reset,
297 isSubmitting,
298 submitError,
299 isValid: Object.values(fields).every(field => field.isValid)
300 };
301}
302
303// =====================================================================
304// Data Management Hook with Caching
305// =====================================================================
306
307function useDataManager() {
308 const [cache, setCache] = useState(new Map());
309 const [loading, setLoading] = useState(new Set());
310 const [errors, setErrors] = useState(new Map());
311
312 const fetchData = useCallback(async (key, fetcher, options = {}) => {
313 const { forceFresh = false, ttl = 300000 } = options; // 5 minutes default TTL
314
315 // Check cache first
316 if (!forceFresh && cache.has(key)) {
317 const cached = cache.get(key);
318 if (Date.now() - cached.timestamp < ttl) {
319 return cached.data;
320 }
321 }
322
323 // Prevent duplicate requests
324 if (loading.has(key)) {
325 return new Promise((resolve, reject) => {
326 const checkCompletion = () => {
327 if (!loading.has(key)) {
328 if (cache.has(key)) {
329 resolve(cache.get(key).data);
330 } else if (errors.has(key)) {
331 reject(errors.get(key));
332 }
333 } else {
334 setTimeout(checkCompletion, 100);
335 }
336 };
337 checkCompletion();
338 });
339 }
340
341 setLoading(prev => new Set(prev).add(key));
342 setErrors(prev => {
343 const newErrors = new Map(prev);
344 newErrors.delete(key);
345 return newErrors;
346 });
347
348 try {
349 const data = await fetcher();
350
351 setCache(prev => new Map(prev).set(key, {
352 data,
353 timestamp: Date.now()
354 }));
355
356 return data;
357 } catch (error) {
358 setErrors(prev => new Map(prev).set(key, error));
359 throw error;
360 } finally {
361 setLoading(prev => {
362 const newLoading = new Set(prev);
363 newLoading.delete(key);
364 return newLoading;
365 });
366 }
367 }, [cache, loading, errors]);
368
369 const invalidateCache = useCallback((key) => {
370 if (key) {
371 setCache(prev => {
372 const newCache = new Map(prev);
373 newCache.delete(key);
374 return newCache;
375 });
376 } else {
377 setCache(new Map());
378 }
379 }, []);
380
381 const getCachedData = useCallback((key) => {
382 return cache.get(key)?.data;
383 }, [cache]);
384
385 const isLoading = useCallback((key) => {
386 return loading.has(key);
387 }, [loading]);
388
389 const getError = useCallback((key) => {
390 return errors.get(key);
391 }, [errors]);
392
393 return {
394 fetchData,
395 invalidateCache,
396 getCachedData,
397 isLoading,
398 getError,
399 cache: Array.from(cache.entries()).map(([key, value]) => ({ key, ...value }))
400 };
401}
402
403// =====================================================================
404// Compound Hook: Advanced Dropdown
405// =====================================================================
406
407function useDropdown() {
408 const { value: isOpen, toggle, setFalse: close } = useToggle();
409 const dropdownRef = useRef(null);
410 const triggerRef = useRef(null);
411
412 useClickOutside(dropdownRef, close);
413
414 const handleKeyDown = useCallback((event) => {
415 if (event.key === 'Escape') {
416 close();
417 }
418 }, [close]);
419
420 useEffect(() => {
421 if (isOpen) {
422 document.addEventListener('keydown', handleKeyDown);
423 return () => document.removeEventListener('keydown', handleKeyDown);
424 }
425 }, [isOpen, handleKeyDown]);
426
427 return {
428 isOpen,
429 toggle,
430 close,
431 dropdownRef,
432 triggerRef,
433 dropdownProps: {
434 ref: dropdownRef,
435 style: { display: isOpen ? 'block' : 'none' }
436 },
437 triggerProps: {
438 ref: triggerRef,
439 onClick: toggle
440 }
441 };
442}
443
444// =====================================================================
445// Demo Components
446// =====================================================================
447
448function AsyncDemo() {
449 const mockApi = useCallback(async (delay = 1000) => {
450 await new Promise(resolve => setTimeout(resolve, delay));
451 if (Math.random() > 0.7) {
452 throw new Error('Random API error for demo');
453 }
454 return `Data loaded at ${new Date().toLocaleTimeString()}`;
455 }, []);
456
457 const { data, loading, error, execute, retry, reset } = useAsync(mockApi, {
458 onSuccess: (data) => console.log('Success:', data),
459 onError: (error) => console.log('Error:', error),
460 retryCount: 3,
461 retryDelay: 1000
462 });
463
464 return (
465 <div className="p-6 border rounded-lg">
466 <h3 className="text-lg font-semibold mb-4">🔄 Advanced Async Hook Demo</h3>
467
468 <div className="space-y-4">
469 <div className="flex gap-2">
470 <button
471 onClick={() => execute()}
472 disabled={loading}
473 className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
474 >
475 {loading ? 'Loading...' : 'Fetch Data'}
476 </button>
477
478 {error && (
479 <button
480 onClick={retry}
481 className="px-4 py-2 bg-orange-500 text-white rounded"
482 >
483 Retry
484 </button>
485 )}
486
487 <button
488 onClick={reset}
489 className="px-4 py-2 bg-gray-500 text-white rounded"
490 >
491 Reset
492 </button>
493 </div>
494
495 {loading && (
496 <div className="flex items-center gap-2">
497 <div className="animate-spin w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full"></div>
498 <span>Loading...</span>
499 </div>
500 )}
501
502 {error && (
503 <div className="p-3 bg-red-100 border border-red-300 rounded text-red-700">
504 Error: {error.message}
505 </div>
506 )}
507
508 {data && (
509 <div className="p-3 bg-green-100 border border-green-300 rounded text-green-700">
510 ✅ {data}
511 </div>
512 )}
513 </div>
514 </div>
515 );
516}
517
518function FormDemo() {
519 const emailValidator = useCallback(async (value) => {
520 if (!value) return 'Email is required';
521 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Invalid email format';
522
523 // Simulate async validation
524 await new Promise(resolve => setTimeout(resolve, 500));
525 if (value === '[email protected]') return 'Email already taken';
526
527 return true;
528 }, []);
529
530 const passwordValidator = useCallback(async (value) => {
531 if (!value) return 'Password is required';
532 if (value.length < 8) return 'Password must be at least 8 characters';
533 if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
534 return 'Password must contain uppercase, lowercase, and number';
535 }
536 return true;
537 }, []);
538
539 const { registerField, handleSubmit, isSubmitting, submitError, isValid } = useForm();
540
541 const emailField = registerField('email', [emailValidator]);
542 const passwordField = registerField('password', [passwordValidator]);
543
544 const onSubmit = useCallback(async (values) => {
545 console.log('Form submitted:', values);
546 await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate API call
547 alert('Form submitted successfully!');
548 }, []);
549
550 return (
551 <div className="p-6 border rounded-lg">
552 <h3 className="text-lg font-semibold mb-4">📝 Advanced Form Hook Demo</h3>
553
554 <form
555 onSubmit={(e) => {
556 e.preventDefault();
557 handleSubmit(onSubmit);
558 }}
559 className="space-y-4"
560 >
561 <div>
562 <label className="block text-sm font-medium mb-1">Email</label>
563 <input
564 type="email"
565 value={emailField.value}
566 onChange={(e) => emailField.handleChange(e.target.value)}
567 onBlur={emailField.handleBlur}
568 className={`w-full p-2 border rounded ${
569 emailField.touched && emailField.errors.length > 0
570 ? 'border-red-500 bg-red-50'
571 : 'border-gray-300'
572 }`}
573 placeholder="Enter your email"
574 />
575 {emailField.touched && emailField.errors.map((error, index) => (
576 <p key={index} className="text-red-500 text-sm mt-1">{error}</p>
577 ))}
578 </div>
579
580 <div>
581 <label className="block text-sm font-medium mb-1">Password</label>
582 <input
583 type="password"
584 value={passwordField.value}
585 onChange={(e) => passwordField.handleChange(e.target.value)}
586 onBlur={passwordField.handleBlur}
587 className={`w-full p-2 border rounded ${
588 passwordField.touched && passwordField.errors.length > 0
589 ? 'border-red-500 bg-red-50'
590 : 'border-gray-300'
591 }`}
592 placeholder="Enter your password"
593 />
594 {passwordField.touched && passwordField.errors.map((error, index) => (
595 <p key={index} className="text-red-500 text-sm mt-1">{error}</p>
596 ))}
597 </div>
598
599 {submitError && (
600 <div className="p-3 bg-red-100 border border-red-300 rounded text-red-700">
601 {submitError}
602 </div>
603 )}
604
605 <button
606 type="submit"
607 disabled={!isValid || isSubmitting}
608 className="w-full py-2 bg-blue-500 text-white rounded disabled:opacity-50"
609 >
610 {isSubmitting ? 'Submitting...' : 'Submit'}
611 </button>
612 </form>
613 </div>
614 );
615}
616
617function DataManagerDemo() {
618 const dataManager = useDataManager();
619 const [selectedUser, setSelectedUser] = useState(null);
620
621 const fetchUsers = useCallback(async () => {
622 // Simulate API call
623 await new Promise(resolve => setTimeout(resolve, 1000));
624 return [
625 { id: 1, name: 'John Doe', email: '[email protected]' },
626 { id: 2, name: 'Jane Smith', email: '[email protected]' },
627 { id: 3, name: 'Bob Johnson', email: '[email protected]' }
628 ];
629 }, []);
630
631 const fetchUserDetails = useCallback(async (userId) => {
632 await new Promise(resolve => setTimeout(resolve, 800));
633 return {
634 id: userId,
635 bio: `Detailed bio for user ${userId}`,
636 lastLogin: new Date().toISOString(),
637 posts: Math.floor(Math.random() * 100)
638 };
639 }, []);
640
641 const loadUsers = useCallback(async () => {
642 try {
643 const users = await dataManager.fetchData('users', fetchUsers);
644 console.log('Users loaded:', users);
645 } catch (error) {
646 console.error('Failed to load users:', error);
647 }
648 }, [dataManager, fetchUsers]);
649
650 const loadUserDetails = useCallback(async (userId) => {
651 try {
652 const details = await dataManager.fetchData(`user-${userId}`, () => fetchUserDetails(userId));
653 setSelectedUser(details);
654 } catch (error) {
655 console.error('Failed to load user details:', error);
656 }
657 }, [dataManager, fetchUserDetails]);
658
659 const users = dataManager.getCachedData('users') || [];
660
661 return (
662 <div className="p-6 border rounded-lg">
663 <h3 className="text-lg font-semibold mb-4">💾 Data Manager Hook Demo</h3>
664
665 <div className="space-y-4">
666 <div className="flex gap-2">
667 <button
668 onClick={loadUsers}
669 disabled={dataManager.isLoading('users')}
670 className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
671 >
672 {dataManager.isLoading('users') ? 'Loading...' : 'Load Users'}
673 </button>
674
675 <button
676 onClick={() => dataManager.invalidateCache('users')}
677 className="px-4 py-2 bg-red-500 text-white rounded"
678 >
679 Clear Cache
680 </button>
681 </div>
682
683 {dataManager.getError('users') && (
684 <div className="p-3 bg-red-100 border border-red-300 rounded text-red-700">
685 Error: {dataManager.getError('users').message}
686 </div>
687 )}
688
689 {users.length > 0 && (
690 <div>
691 <h4 className="font-semibold mb-2">Users:</h4>
692 <div className="space-y-2">
693 {users.map(user => (
694 <div key={user.id} className="flex items-center justify-between p-2 border rounded">
695 <span>{user.name} ({user.email})</span>
696 <button
697 onClick={() => loadUserDetails(user.id)}
698 disabled={dataManager.isLoading(`user-${user.id}`)}
699 className="px-3 py-1 bg-green-500 text-white rounded text-sm disabled:opacity-50"
700 >
701 {dataManager.isLoading(`user-${user.id}`) ? 'Loading...' : 'Details'}
702 </button>
703 </div>
704 ))}
705 </div>
706 </div>
707 )}
708
709 {selectedUser && (
710 <div className="p-4 bg-blue-50 border border-blue-200 rounded">
711 <h4 className="font-semibold mb-2">User Details:</h4>
712 <p><strong>Bio:</strong> {selectedUser.bio}</p>
713 <p><strong>Posts:</strong> {selectedUser.posts}</p>
714 <p><strong>Last Login:</strong> {new Date(selectedUser.lastLogin).toLocaleString()}</p>
715 </div>
716 )}
717
718 <div className="mt-4 p-3 bg-gray-50 rounded">
719 <h4 className="font-semibold mb-2">Cache Status:</h4>
720 <div className="text-sm space-y-1">
721 {dataManager.cache.length === 0 ? (
722 <p>No cached data</p>
723 ) : (
724 dataManager.cache.map(({ key, timestamp }) => (
725 <div key={key} className="flex justify-between">
726 <span>{key}</span>
727 <span className="text-gray-500">
728 {Math.round((Date.now() - timestamp) / 1000)}s ago
729 </span>
730 </div>
731 ))
732 )}
733 </div>
734 </div>
735 </div>
736 </div>
737 );
738}
739
740function DropdownDemo() {
741 const dropdown = useDropdown();
742 const [settings, setSettings] = useLocalStorage('demo-settings', { theme: 'light', notifications: true });
743
744 return (
745 <div className="p-6 border rounded-lg">
746 <h3 className="text-lg font-semibold mb-4">🎛️ Compound Hook Demo (Dropdown + Local Storage)</h3>
747
748 <div className="relative">
749 <button
750 {...dropdown.triggerProps}
751 className="px-4 py-2 bg-purple-500 text-white rounded"
752 >
753 Settings {dropdown.isOpen ? '▲' : '▼'}
754 </button>
755
756 <div
757 {...dropdown.dropdownProps}
758 className="absolute top-full left-0 mt-2 w-64 bg-white border rounded-lg shadow-lg z-10 p-4"
759 >
760 <h4 className="font-semibold mb-3">App Settings</h4>
761
762 <div className="space-y-3">
763 <div>
764 <label className="block text-sm font-medium mb-1">Theme</label>
765 <select
766 value={settings.theme}
767 onChange={(e) => setSettings({ ...settings, theme: e.target.value })}
768 className="w-full p-2 border rounded"
769 >
770 <option value="light">Light</option>
771 <option value="dark">Dark</option>
772 <option value="auto">Auto</option>
773 </select>
774 </div>
775
776 <div className="flex items-center gap-2">
777 <input
778 type="checkbox"
779 id="notifications"
780 checked={settings.notifications}
781 onChange={(e) => setSettings({ ...settings, notifications: e.target.checked })}
782 />
783 <label htmlFor="notifications" className="text-sm">Enable notifications</label>
784 </div>
785
786 <div className="pt-2 border-t">
787 <p className="text-xs text-gray-500">
788 Settings are automatically saved to localStorage
789 </p>
790 </div>
791 </div>
792 </div>
793 </div>
794
795 <div className="mt-4 p-3 bg-gray-50 rounded">
796 <h4 className="font-semibold mb-2">Current Settings:</h4>
797 <pre className="text-sm">{JSON.stringify(settings, null, 2)}</pre>
798 </div>
799 </div>
800 );
801}
802
803// =====================================================================
804// Main Demo App
805// =====================================================================
806
807export default function CustomHooksDemo() {
808 const [searchTerm, setSearchTerm] = useState('');
809 const debouncedSearch = useDebounce(searchTerm, 300);
810
811 useEffect(() => {
812 if (debouncedSearch) {
813 console.log('Searching for:', debouncedSearch);
814 }
815 }, [debouncedSearch]);
816
817 return (
818 <div className="min-h-screen bg-gray-100 py-8">
819 <div className="container mx-auto px-4">
820 <div className="bg-white rounded-lg shadow-lg overflow-hidden">
821 <div className="bg-gradient-to-r from-purple-600 to-pink-600 text-white p-6">
822 <h1 className="text-3xl font-bold mb-2">🪝 Advanced Custom Hooks Library</h1>
823 <p className="text-purple-100">
824 Comprehensive demonstration of custom hook patterns including async operations,
825 form management, data caching, and compound hook architectures
826 </p>
827 </div>
828
829 <div className="p-6">
830 <div className="mb-6">
831 <label className="block text-sm font-medium mb-2">🔍 Debounced Search Demo</label>
832 <input
833 type="text"
834 value={searchTerm}
835 onChange={(e) => setSearchTerm(e.target.value)}
836 placeholder="Type to see debounced search in console..."
837 className="w-full p-3 border rounded-lg"
838 />
839 {debouncedSearch && (
840 <p className="text-sm text-gray-600 mt-1">
841 Debounced value: "{debouncedSearch}"
842 </p>
843 )}
844 </div>
845
846 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
847 <AsyncDemo />
848 <FormDemo />
849 <DataManagerDemo />
850 <DropdownDemo />
851 </div>
852
853 <div className="mt-8 p-6 bg-gray-50 rounded-lg">
854 <h3 className="text-lg font-semibold mb-4">🎯 Custom Hook Patterns Demonstrated</h3>
855 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
856 <div className="bg-white p-3 rounded border">
857 <strong>✅ Async Operations</strong>
858 <p>Complete async state management with retries and cancellation</p>
859 </div>
860 <div className="bg-white p-3 rounded border">
861 <strong>✅ Form Management</strong>
862 <p>Advanced form state with validation and error handling</p>
863 </div>
864 <div className="bg-white p-3 rounded border">
865 <strong>✅ Data Caching</strong>
866 <p>Intelligent caching with TTL and invalidation strategies</p>
867 </div>
868 <div className="bg-white p-3 rounded border">
869 <strong>✅ Compound Hooks</strong>
870 <p>Complex UI patterns combining multiple hook concerns</p>
871 </div>
872 <div className="bg-white p-3 rounded border">
873 <strong>✅ Local Storage</strong>
874 <p>Persistent state with automatic serialization</p>
875 </div>
876 <div className="bg-white p-3 rounded border">
877 <strong>✅ Utility Hooks</strong>
878 <p>Debouncing, click outside, and other common patterns</p>
879 </div>
880 </div>
881 </div>
882 </div>
883 </div>
884 </div>
885 </div>
886 );
887}This comprehensive custom hooks demonstration showcases advanced patterns for building reusable, production-ready hook libraries:
🏗️ Hook Development Patterns:
1. Utility Hooks Foundation:
2. Advanced Async Hook (useAsync):
3. Sophisticated Form Management:
4. Data Manager with Caching:
5. Compound Hook Patterns:
6. Production-Ready Features:
7. Real-World Integration:
8. Testing Considerations:
This hook library demonstrates how to build enterprise-grade custom hooks that are reusable, maintainable, and provide excellent developer experience while handling complex real-world scenarios effectively.
Outstanding work mastering custom hook development! You now have the skills to create sophisticated, reusable hook libraries for any React application. In our next lesson, we'll explore useCallback and useMemo Optimization, where you'll learn advanced performance optimization techniques, memoization strategies, and how to prevent unnecessary re-renders in complex component hierarchies.
Your progress is saved in this browser, on this device only.