As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!
JavaScript has revolutionized modern web development, and React's introduction of hooks transformed how we build components. In my years of React development, I've found that custom hooks offer exceptional power for code organization and reuse. Here are six techniques I use to build efficient custom React hooks that boost performance and maintainability.
When I create custom React hooks, I focus on making them lean, purposeful, and optimized. These tools have become essential in my development toolkit, helping me solve complex problems with elegant solutions.
Clean Dependency Management
Managing dependencies correctly is critical for optimal hook performance. I've learned that imprecise dependency arrays lead to unnecessary re-renders and calculations.
// Problematic approach
function useUserData(userId) {
const [userData, setUserData] = useState(null);
// This object creates a new reference every render
const options = { credentials: 'include' };
useEffect(() => {
fetch(`/api/users/${userId}`, options)
.then(res => res.json())
.then(data => setUserData(data));
}, [userId, options]); // options changes every render!
return userData;
}
// Improved approach
function useUserData(userId) {
const [userData, setUserData] = useState(null);
// Memoize the options object
const options = useMemo(() => ({
credentials: 'include'
}), []); // Empty dependency array - never changes
useEffect(() => {
fetch(`/api/users/${userId}`, options)
.then(res => res.json())
.then(data => setUserData(data));
}, [userId, options]); // options is stable now
return userData;
}
I always extract static values outside the hook body and use useMemo()
for complex objects. For functions, I prefer useCallback()
to maintain referential stability. This attention to dependency management has eliminated many performance issues in my applications.
State Initialization Patterns
Proper state initialization can significantly impact hook performance. I've adopted lazy initialization to avoid expensive calculations during renders.
// Without lazy initialization
function useLocalStorageState(key, defaultValue) {
// This runs on EVERY render
const storedValue = JSON.parse(localStorage.getItem(key)) || defaultValue;
const [state, setState] = useState(storedValue);
// Rest of hook logic...
return [state, setState];
}
// With lazy initialization
function useLocalStorageState(key, defaultValue) {
// This runs only on initial render
const [state, setState] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
console.error("Error reading from localStorage:", error);
return defaultValue;
}
});
// Rest of hook logic...
return [state, setState];
}
When I need to compute initial state from props, I always use the function form of useState()
. This ensures the calculation happens only once during mounting, rather than on every render cycle. This pattern has been particularly valuable in data-intensive applications I've built.
Effect Cleanup Optimization
Proper cleanup is essential for hooks that create subscriptions, intervals, or listeners. I've seen many memory leaks from improper cleanup, which I now meticulously avoid.
// Basic interval hook with proper cleanup
function useInterval(callback, delay) {
const savedCallback = useRef(callback);
// Keep the callback reference fresh
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const tick = () => savedCallback.current();
const id = setInterval(tick, delay);
// Clean up on unmount or delay change
return () => clearInterval(id);
}, [delay]);
}
// More complex example with event management
function useWindowResize(handler) {
const handlerRef = useRef(handler);
useEffect(() => {
handlerRef.current = handler;
}, [handler]);
useEffect(() => {
const handleResize = (event) => {
handlerRef.current(event);
};
window.addEventListener('resize', handleResize);
// Return cleanup function
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Empty dependency array means setup and cleanup run once
}
I ensure cleanup functions handle all resources, particularly for asynchronous operations. Using AbortController
for fetch requests has become standard practice in my hooks, preventing race conditions and memory leaks.
Composition Strategy
Breaking complex hooks into smaller ones has transformed my code organization. I apply the single responsibility principle rigorously when designing custom hooks.
// Instead of one massive hook that does everything...
function useProductPage(productId) {
// Decompose into smaller, focused hooks
const product = useProduct(productId);
const { addToCart, isAddingToCart } = useShoppingCart();
const { isFavorite, toggleFavorite } = useFavorites(productId);
const { reviews, isLoadingReviews } = useProductReviews(productId);
// Combine results if needed
return {
product,
reviews,
isLoadingReviews,
isFavorite,
toggleFavorite,
addToCart,
isAddingToCart
};
}
// Example of one of the smaller hooks
function useProduct(productId) {
const [product, setProduct] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
setIsLoading(true);
fetchProduct(productId)
.then(data => {
if (isMounted) {
setProduct(data);
setIsLoading(false);
}
})
.catch(err => {
if (isMounted) {
setError(err);
setIsLoading(false);
}
});
return () => { isMounted = false; };
}, [productId]);
return { product, isLoading, error };
}
This compositional approach has made my hooks more testable and reusable. I can mix and match them like building blocks, avoiding the monolithic custom hooks that plagued my earlier React projects.
Custom Equality Functions
Default equality checks don't always align with application needs. I've implemented specialized equality functions to prevent unnecessary re-renders.
function useDeepCompareMemo(value, dependencies) {
const ref = useRef();
// Only update if deep comparison shows changes
const areEqual = ref.current ? isEqual(dependencies, ref.current) : false;
useEffect(() => {
if (!areEqual) {
ref.current = dependencies;
}
});
return useMemo(() => value, [areEqual]);
}
// Implementation example
function useFetchWithCache(url, options, deps = []) {
return useDeepCompareMemo(() => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true;
fetchData(url, options)
.then(result => {
if (isMounted) setData(result);
})
.finally(() => {
if (isMounted) setLoading(false);
});
return () => { isMounted = false; };
}, [url, options]);
return [data, loading];
}, [url, options, ...deps]);
}
For data-heavy applications, I often implement specialized comparison logic that understands my domain objects. This prevents wasteful re-renders when the meaningful data hasn't changed, despite reference changes.
Extraction and Testing
Testing hooks directly can be challenging. I've adopted a pattern of extracting pure business logic from hooks to enhance testability.
// Extract pure calculation logic
export function calculatePagination(totalItems, itemsPerPage, currentPage) {
const totalPages = Math.ceil(totalItems / itemsPerPage);
const validCurrentPage = Math.max(1, Math.min(currentPage, totalPages));
return {
totalPages,
currentPage: validCurrentPage,
hasNextPage: validCurrentPage < totalPages,
hasPrevPage: validCurrentPage > 1
};
}
// Use the pure function inside the hook
export function usePagination(totalItems, itemsPerPage, initialPage = 1) {
const [currentPage, setCurrentPage] = useState(initialPage);
// Calculate pagination using the pure function
const pagination = useMemo(() =>
calculatePagination(totalItems, itemsPerPage, currentPage),
[totalItems, itemsPerPage, currentPage]
);
const goToPage = useCallback((page) => {
setCurrentPage(Math.max(1, Math.min(page, pagination.totalPages)));
}, [pagination.totalPages]);
const nextPage = useCallback(() => {
if (pagination.hasNextPage) {
setCurrentPage(p => p + 1);
}
}, [pagination.hasNextPage]);
const prevPage = useCallback(() => {
if (pagination.hasPrevPage) {
setCurrentPage(p => p - 1);
}
}, [pagination.hasPrevPage]);
return {
...pagination,
goToPage,
nextPage,
prevPage
};
}
// Now calculatePagination can be easily tested independently
This separation has significantly improved my test coverage. I can test the pure functions thoroughly with simple unit tests, while using React Testing Library for the hook's integration with React lifecycle.
Practical Example: A Complete Hook
Here's a comprehensive example that applies these techniques to create a robust, efficient search hook:
// Pure function for filtering
function filterItems(items, searchTerm, filterFn) {
if (!searchTerm.trim()) return items;
return items.filter(item => filterFn(item, searchTerm));
}
// Custom hook implementation
function useSearch(items, options = {}) {
// Default options with memoization
const config = useMemo(() => ({
initialSearchTerm: '',
debounceTime: 300,
filterFn: (item, term) =>
String(item).toLowerCase().includes(term.toLowerCase()),
...options
}), [options]);
// State with proper initialization
const [searchTerm, setSearchTerm] = useState(config.initialSearchTerm);
const [debouncedTerm, setDebouncedTerm] = useState(searchTerm);
// Debounce effect
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedTerm(searchTerm);
}, config.debounceTime);
return () => {
clearTimeout(handler);
};
}, [searchTerm, config.debounceTime]);
// Memoized filtered results
const filteredItems = useMemo(() =>
filterItems(items, debouncedTerm, config.filterFn),
[items, debouncedTerm, config.filterFn]
);
// Memoized stats
const stats = useMemo(() => ({
total: items.length,
filtered: filteredItems.length,
searching: debouncedTerm !== searchTerm
}), [items.length, filteredItems.length, debouncedTerm, searchTerm]);
// Event handler with useCallback
const handleSearchChange = useCallback((e) => {
setSearchTerm(typeof e === 'string' ? e : e.target.value);
}, []);
return {
searchTerm,
results: filteredItems,
stats,
handleSearchChange,
resetSearch: useCallback(() => setSearchTerm(''), [])
};
}
This hook showcases clean dependency management, proper state initialization, composition, and extraction of pure logic. It's efficient, maintainable, and suitable for production applications.
Final Thoughts
Creating efficient custom React hooks requires attention to detail and a solid understanding of React's rendering behavior. The techniques I've shared come from real-world experiences building production applications.
Custom hooks have transformed how I organize React code. They've helped me extract and reuse logic across components while maintaining clean, readable code. By applying these six techniques, I've eliminated common performance issues and created more robust applications.
When building your own custom hooks, remember that simplicity often leads to better performance. Start with focused, single-purpose hooks and compose them as needed. Test your hooks thoroughly, especially edge cases around cleanup and initialization.
These patterns have served me well across projects of various sizes and complexities. I'm confident they'll help you create more efficient, maintainable React applications too.
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva
Top comments (0)