Custom Hooks Patterns
Learn advanced custom hooks patterns for complex state management and side effects
85 min•By Priygop Team•Last 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 WebSocketTry 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