React Hooks have made functional components incredibly powerful, but taking them to the next level unlocks even more potential. This post dives into advanced useEffect patterns, performance wins with useMemo, and battle-tested custom Hooks that’ll make your code cleaner and more reusable.
Advanced useEffect Patterns
useEffect handles side effects, but the real magic happens when you master cleanup functions and dependency tuning. That return function from useEffect runs right before the next effect or when the component unmountsâperfect for preventing memory leaks.
Take window resize tracking:
import { useState, useEffect } from 'react';
function WindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const updateSize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener('resize', updateSize);
updateSize(); // Kick it off immediately
return () => window.removeEventListener('resize', updateSize);
}, []); // Empty deps = runs once
return <div>Window size: {size.width} Ã {size.height}</div>;
}Pro tip: Swap in useLayoutEffect for DOM measurementsâit runs synchronously before paint, no flash.
Performance Boosts with useMemo & useCallback
Child components re-rendering on every parent update? Thatâs where useMemo (for expensive calculations) and useCallback (for stable functions) save the day.
Real-world product filtering:
function ProductList({ products, searchTerm }) {
const filteredProducts = useMemo(() => {
return products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [products, searchTerm]); // Recalculate only when these change
const handleSort = useCallback((sortBy) => {
// Sorting logic here
}, []);
return (
<div>
{filteredProducts.map(product => (
<Product key={product.id} onSort={handleSort} />
))}
</div>
);
}Result: With thousands of products, filtering happens once per keystroke instead of every render.
Custom Hooks That Actually Ship
Custom Hooks start with âuseâ, call other Hooks internally, and return reusable logic. Hereâs three Iâve used in production:
1. useDebounce - Tame Search Inputs
import { useState, useEffect } from 'react';
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// In your search component:
function SearchInput() {
const [input, setInput] = useState('');
const debouncedSearch = useDebounce(input, 500);
useEffect(() => {
if (debouncedSearch) {
fetchProducts(debouncedSearch); // API call
}
}, [debouncedSearch]);
return (
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Search products..."
/>
);
}2. useToggle - Simple On/Off States
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue(prev => !prev);
const setTrue = () => setValue(true);
const setFalse = () => setValue(false);
return [value, { toggle, setTrue, setFalse }];
}
// Usage: const [isOpen, { toggle }] = useToggle(false);3. useLocalStorage - Persist State Effortlessly
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
if (typeof window === 'undefined') return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setStoredValue = (newValue) => {
try {
setValue(newValue);
window.localStorage.setItem(key, JSON.stringify(newValue));
} catch (error) {
console.error(error);
}
};
return [value, setStoredValue];
}Pitfalls and Hard-Won Lessons
Functional updates: Always setCount(prev => prev + 1) to grab the latest state, dodging stale closures.
Full deps: Let eslint-plugin-react-hooks catch missing dependencies.
Complex state: Reach for useReducer over multiple useState calls.
The rules: Top-level only. No conditionals or loops.
Scenario | Hook Combo | Win |
|---|---|---|
Search input | useDebounce + useEffect | 80% fewer API calls |
Large lists | useMemo + useCallback | 3x render speedup |
Forms | useReducer + useCallback | Predictable state |
Persistence | useLocalStorage | Instant sync |
These patterns have cleaned up my e-commerce apps big time. Drop your favorite custom Hooks in the comments!