Advanced React Hook Patterns: Building Custom Hooks for Real-World Applications
Introduction
Custom React hooks are one of the most powerful features for creating reusable and maintainable code. While many developers understand the basics, mastering advanced patterns can significantly improve your application architecture. In this post, we'll explore practical custom hook patterns that solve real-world problems you encounter daily as a full-stack developer.
1. The useAPI Hook Pattern
One of the most common patterns is abstracting API calls into reusable hooks. Here's a robust implementation:
import { useState, useEffect, useCallback } from 'react';
function useAPI(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [url, options]);
useEffect(() => {
fetchData();
}, [fetchData]);
const refetch = useCallback(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch };
}Usage in your components becomes incredibly clean:
function UserProfile({ userId }) {
const { data: user, loading, error, refetch } = useAPI(`/api/users/${userId}`);
if (loading) return Loading...;
if (error) return Error: {error};
return (
{user.name}
);
}2. Smart Local Storage Hook
Managing local storage with React can be tricky. This hook handles serialization, SSR compatibility, and synchronization across tabs:
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Get value from localStorage or use initial value
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === 'undefined') {
return initialValue;
}
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;
}
});
// Return a wrapped version of useState's setter function
const setValue = (value) => {
try {
// Allow value to be a function for same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
// Save to local storage
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};
// Listen for changes in other tabs
useEffect(() => {
const handleStorageChange = (e) => {
if (e.key === key && e.newValue !== null) {
try {
setStoredValue(JSON.parse(e.newValue));
} catch (error) {
console.error('Error parsing storage event data:', error);
}
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, [key]);
return [storedValue, setValue];
}3. Advanced Form Handling Hook
Forms are everywhere in web applications. This hook handles validation, submission states, and error management:
import { useState, useCallback } from 'react';
function useForm(initialValues, validationRules = {}) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const validateField = useCallback((name, value) => {
const rule = validationRules[name];
if (!rule) return '';
if (rule.required && (!value || value.toString().trim() === '')) {
return `${name} is required`;
}
if (rule.minLength && value.length < rule.minLength) {
return `${name} must be at least ${rule.minLength} characters`;
}
if (rule.pattern && !rule.pattern.test(value)) {
return rule.message || `${name} format is invalid`;
}
return '';
}, [validationRules]);
const handleChange = useCallback((name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
// Validate on change if field was previously touched
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(validationRules).forEach(name => {
const error = validateField(name, values[name]);
if (error) newErrors[name] = error;
});
setErrors(newErrors);
setTouched(Object.keys(validationRules).reduce((acc, key) => ({ ...acc, [key]: true }), {}));
if (Object.keys(newErrors).length === 0) {
try {
await onSubmit(values);
} catch (error) {
console.error('Form submission error:', error);
}
}
setIsSubmitting(false);
}, [values, validationRules, validateField]);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
setIsSubmitting(false);
}, [initialValues]);
return {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
reset,
isValid: Object.keys(errors).length === 0
};
}Using the form hook:
function ContactForm() {
const { values, errors, handleChange, handleBlur, handleSubmit, isSubmitting } = useForm(
{ name: '', email: '', message: '' },
{
name: { required: true, minLength: 2 },
email: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'Please enter a valid email'
},
message: { required: true, minLength: 10 }
}
);
const onSubmit = async (formData) => {
await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
};
return (
);
}Best Practices for Custom Hooks
When building custom hooks, follow these guidelines:
- Single Responsibility: Each hook should handle one specific concern
- Memoization: Use
useCallbackanduseMemoto prevent unnecessary re-renders - Error Handling: Always include proper error boundaries and fallback states
- TypeScript: Add proper typing for better developer experience
- Testing: Custom hooks should be thoroughly tested with React Testing Library
Conclusion
These advanced hook patterns provide a solid foundation for building scalable React applications. By abstracting common functionality into reusable hooks, you create cleaner components and more maintainable codebases. The patterns shown here handle real-world scenarios you'll encounter in production applications, making them immediately practical for your next project.