Skip to main content
Course/Module 7/Topic 4 of 4Advanced

Custom Hooks Patterns

Learn advanced custom hooks patterns for complex state management and side effects

85 minBy Priygop TeamLast updated: Feb 2026

Advanced Custom Hooks

Example
// Advanced custom hooks for complex scenarios
import { useState, useEffect, useCallback, useRef } from 'react';

// Hook for managing form state with validation
function useForm(initialValues = {}, validationSchema = {}) {
    const [values, setValues] = useState(initialValues);
    const [errors, setErrors] = useState({});
    const [touched, setTouched] = useState({});
    const [isSubmitting, setIsSubmitting] = useState(false);

    const validateField = useCallback((name, value) => {
        const rules = validationSchema[name];
        if (!rules) return '';

        for (const rule of rules) {
            if (rule.required && !value) {
                return rule.message || `${name} is required`;
            }
            if (rule.minLength && value.length < rule.minLength) {
                return rule.message || `${name} must be at least ${rule.minLength} characters`;
            }
            if (rule.pattern && !rule.pattern.test(value)) {
                return rule.message || `${name} format is invalid`;
            }
        }
        return '';
    }, [validationSchema]);

    const handleChange = useCallback((name, value) => {
        setValues(prev => ({ ...prev, [name]: value }));
        if (touched[name]) {
            const error = validateField(name, value);
            setErrors(prev => ({ ...prev, [name]: error }));
        }
    }, [touched, validateField]);

    const handleBlur = useCallback((name) => {
        setTouched(prev => ({ ...prev, [name]: true }));
        const error = validateField(name, values[name]);
        setErrors(prev => ({ ...prev, [name]: error }));
    }, [values, validateField]);

    const handleSubmit = useCallback(async (onSubmit) => {
        setIsSubmitting(true);
        
        // Validate all fields
        const newErrors = {};
        Object.keys(validationSchema).forEach(name => {
            const error = validateField(name, values[name]);
            if (error) newErrors[name] = error;
        });

        setErrors(newErrors);
        setTouched(Object.keys(validationSchema).reduce((acc, key) => ({ ...acc, [key]: true }), {}));

        if (Object.keys(newErrors).length === 0) {
            await onSubmit(values);
        }
        
        setIsSubmitting(false);
    }, [values, validationSchema, validateField]);

    const reset = useCallback(() => {
        setValues(initialValues);
        setErrors({});
        setTouched({});
        setIsSubmitting(false);
    }, [initialValues]);

    return {
        values,
        errors,
        touched,
        isSubmitting,
        handleChange,
        handleBlur,
        handleSubmit,
        reset
    };
}

// Hook for API calls with caching
function useApi(url, options = {}) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    const cacheRef = useRef(new Map());
    const abortControllerRef = useRef(null);

    const fetchData = useCallback(async () => {
        if (abortControllerRef.current) {
            abortControllerRef.current.abort();
        }

        abortControllerRef.current = new AbortController();

        // Check cache first
        if (options.cache && cacheRef.current.has(url)) {
            const cached = cacheRef.current.get(url);
            if (Date.now() - cached.timestamp < options.cacheTime) {
                setData(cached.data);
                setLoading(false);
                return;
            }
        }

        try {
            setLoading(true);
            setError(null);

            const response = await fetch(url, {
                ...options,
                signal: abortControllerRef.current.signal
            });

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            const result = await response.json();
            setData(result);

            // Cache the result
            if (options.cache) {
                cacheRef.current.set(url, {
                    data: result,
                    timestamp: Date.now()
                });
            }
        } catch (err) {
            if (err.name !== 'AbortError') {
                setError(err.message);
            }
        } finally {
            setLoading(false);
        }
    }, [url, options]);

    useEffect(() => {
        fetchData();
        return () => {
            if (abortControllerRef.current) {
                abortControllerRef.current.abort();
            }
        };
    }, [fetchData]);

    const refetch = useCallback(() => {
        fetchData();
    }, [fetchData]);

    return { data, loading, error, refetch };
}

// Hook for managing local storage
function useLocalStorage(key, initialValue) {
    const [storedValue, setStoredValue] = useState(() => {
        try {
            const item = window.localStorage.getItem(key);
            return item ? JSON.parse(item) : initialValue;
        } catch (error) {
            console.error(`Error reading localStorage key "${key}":`, error);
            return initialValue;
        }
    });

    const setValue = useCallback((value) => {
        try {
            const valueToStore = value instanceof Function ? value(storedValue) : value;
            setStoredValue(valueToStore);
            window.localStorage.setItem(key, JSON.stringify(valueToStore));
        } catch (error) {
            console.error(`Error setting localStorage key "${key}":`, error);
        }
    }, [key, storedValue]);

    return [storedValue, setValue];
}

// Hook for managing window size
function useWindowSize() {
    const [windowSize, setWindowSize] = useState({
        width: window.innerWidth,
        height: window.innerHeight
    });

    useEffect(() => {
        const handleResize = () => {
            setWindowSize({
                width: window.innerWidth,
                height: window.innerHeight
            });
        };

        window.addEventListener('resize', handleResize);
        return () => window.removeEventListener('resize', handleResize);
    }, []);

    return windowSize;
}

// Usage examples
function LoginForm() {
    const validationSchema = {
        email: [
            { required: true, message: 'Email is required' },
            { pattern: /^[^s@]+@[^s@]+.[^s@]+$/, message: 'Invalid email format' }
        ],
        password: [
            { required: true, message: 'Password is required' },
            { minLength: 8, message: 'Password must be at least 8 characters' }
        ]
    };

    const { values, errors, touched, isSubmitting, handleChange, handleBlur, handleSubmit } = useForm({}, validationSchema);

    const onSubmit = async (formData) => {
        console.log('Form submitted:', formData);
        // Handle form submission
    };

    return (
        <form onSubmit={(e) => { e.preventDefault(); handleSubmit(onSubmit); }}>
            <div>
                <input
                    type="email"
                    value={values.email || ''}
                    onChange={(e) => handleChange('email', e.target.value)}
                    onBlur={() => handleBlur('email')}
                    placeholder="Email"
                />
                {touched.email && errors.email && <span className="error">{errors.email}</span>}
            </div>
            <div>
                <input
                    type="password"
                    value={values.password || ''}
                    onChange={(e) => handleChange('password', e.target.value)}
                    onBlur={() => handleBlur('password')}
                    placeholder="Password"
                />
                {touched.password && errors.password && <span className="error">{errors.password}</span>}
            </div>
            <button type="submit" disabled={isSubmitting}>
                {isSubmitting ? 'Submitting...' : 'Login'}
            </button>
        </form>
    );
}

Practice Exercise: Advanced Patterns

Example
// Exercise: Build a Complete Data Management System
// Create a comprehensive data management system using advanced patterns

// Custom hook for managing data with CRUD operations
function useDataManager(initialData = []) {
    const [data, setData] = useState(initialData);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);
    const [filters, setFilters] = useState({});
    const [sortBy, setSortBy] = useState(null);
    const [sortDirection, setSortDirection] = useState('asc');

    // Filter and sort data
    const processedData = useMemo(() => {
        let result = [...data];

        // Apply filters
        Object.entries(filters).forEach(([key, value]) => {
            if (value) {
                result = result.filter(item => 
                    item[key].toLowerCase().includes(value.toLowerCase())
                );
            }
        });

        // Apply sorting
        if (sortBy) {
            result.sort((a, b) => {
                const aValue = a[sortBy];
                const bValue = b[sortBy];
                
                if (sortDirection === 'asc') {
                    return aValue > bValue ? 1 : -1;
                } else {
                    return aValue < bValue ? 1 : -1;
                }
            });
        }

        return result;
    }, [data, filters, sortBy, sortDirection]);

    const addItem = useCallback((item) => {
        setData(prev => [...prev, { ...item, id: Date.now() }]);
    }, []);

    const updateItem = useCallback((id, updates) => {
        setData(prev => prev.map(item => 
            item.id === id ? { ...item, ...updates } : item
        ));
    }, []);

    const deleteItem = useCallback((id) => {
        setData(prev => prev.filter(item => item.id !== id));
    }, []);

    const setFilter = useCallback((key, value) => {
        setFilters(prev => ({ ...prev, [key]: value }));
    }, []);

    const setSorting = useCallback((key) => {
        if (sortBy === key) {
            setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');
        } else {
            setSortBy(key);
            setSortDirection('asc');
        }
    }, [sortBy]);

    return {
        data: processedData,
        loading,
        error,
        addItem,
        updateItem,
        deleteItem,
        setFilter,
        setSorting,
        sortBy,
        sortDirection,
        filters
    };
}

// Compound component for data table
const DataTableContext = createContext();

function DataTable({ children, data, onSort, sortBy, sortDirection }) {
    return (
        <DataTableContext.Provider value={{ data, onSort, sortBy, sortDirection }}>
            <table className="data-table">
                {children}
            </table>
        </DataTableContext.Provider>
    );
}

function DataTableHeader({ children, sortKey }) {
    const { onSort, sortBy, sortDirection } = useContext(DataTableContext);
    
    return (
        <th 
            onClick={() => onSort(sortKey)}
            className={`sortable ${sortBy === sortKey ? 'sorted' : ''}`}
        >
            {children}
            {sortBy === sortKey && (
                <span className={`arrow ${sortDirection}`}>▼</span>
            )}
        </th>
    );
}

function DataTableBody({ children }) {
    const { data } = useContext(DataTableContext);
    
    return (
        <tbody>
            {data.map((item, index) => 
                children(item, index)
            )}
        </tbody>
    );
}

// HOC for adding search functionality
function withSearch(WrappedComponent) {
    return function SearchableComponent(props) {
        const [searchTerm, setSearchTerm] = useState('');
        
        const filteredProps = {
            ...props,
            data: props.data.filter(item => 
                Object.values(item).some(value => 
                    value.toString().toLowerCase().includes(searchTerm.toLowerCase())
                )
            )
        };

        return (
            <div>
                <input
                    type="text"
                    placeholder="Search..."
                    value={searchTerm}
                    onChange={(e) => setSearchTerm(e.target.value)}
                    className="search-input"
                />
                <WrappedComponent {...filteredProps} />
            </div>
        );
    };
}

// Usage example
function UserManagement() {
    const {
        data,
        addItem,
        updateItem,
        deleteItem,
        setSorting,
        sortBy,
        sortDirection
    } = useDataManager([
        { id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' },
        { id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User' },
        { id: 3, name: 'Bob Johnson', email: 'bob@example.com', role: 'User' }
    ]);

    const SearchableDataTable = withSearch(DataTable);

    return (
        <div className="user-management">
            <h1>User Management</h1>
            
            <SearchableDataTable
                data={data}
                onSort={setSorting}
                sortBy={sortBy}
                sortDirection={sortDirection}
            >
                <thead>
                    <tr>
                        <DataTableHeader sortKey="name">Name</DataTableHeader>
                        <DataTableHeader sortKey="email">Email</DataTableHeader>
                        <DataTableHeader sortKey="role">Role</DataTableHeader>
                        <th>Actions</th>
                    </tr>
                </thead>
                <DataTableBody>
                    {(user) => (
                        <tr key={user.id}>
                            <td>{user.name}</td>
                            <td>{user.email}</td>
                            <td>{user.role}</td>
                            <td>
                                <button onClick={() => updateItem(user.id, { role: 'Admin' })}>
                                    Make Admin
                                </button>
                                <button onClick={() => deleteItem(user.id)}>
                                    Delete
                                </button>
                            </td>
                        </tr>
                    )}
                </DataTableBody>
            </SearchableDataTable>
        </div>
    );
}

// Challenge: Add pagination to the data table
// Challenge: Add bulk selection and operations
// Challenge: Add real-time updates with WebSocket

Try It Yourself — Advanced React Patterns

Try It Yourself — Advanced React PatternsHTML
HTML Editor
✓ ValidTab = 2 spaces
HTML|53 lines|2159 chars|✓ Valid syntax
UTF-8

Quick Quiz — Advanced React Patterns

Additional Resources

Recommended Reading

  • React Design Patterns
  • Advanced React Patterns
  • Custom Hooks Best Practices

Online Resources

  • Compound Components Tutorial
  • Render Props Guide
  • HOC Patterns Examples
Chat on WhatsApp
Priygop - Leading Professional Development Platform | Expert Courses & Interview Prep