Mastering React Hooks: Beyond the Basics
React Hooks revolutionized how we write components, but with great power comes great responsibility. Let's explore advanced patterns that will make your code more maintainable and performant.
Understanding the Hook Rules
Before diving deep, let's reinforce the fundamentals:
- Only call hooks at the top level - No hooks inside loops, conditions, or nested functions
- Only call hooks from React functions - Components or custom hooks
These rules exist because React relies on the order of hook calls to maintain state.
useEffect: The Swiss Army Knife
Dependency Array Deep Dive
The dependency array is where most bugs originate:
useEffect(() => {
fetchUser(userId);
}, [userId]); // ✅ Only re-run when userId changes
Common mistake: forgetting to include all dependencies:
const [count, setCount] = useState(0);
useEffect(() => {
console.log(count);
// Stale closure if count not in dependencies
}, []); // ❌ Missing dependency
Cleanup Functions
Proper cleanup prevents memory leaks:
useEffect(() => {
const subscription = dataSource.subscribe();
return () => {
subscription.unsubscribe();
};
}, [dataSource]);
useMemo and useCallback: When to Use Them
The Golden Rule
Don't optimize prematurely. Only use these hooks when:
- You're passing callbacks to optimized child components
- You're computing expensive values
- You're breaking referential equality
Practical Example
const ProductList = ({ products, filter }) => {
// Only recalculate when products or filter change
const filteredProducts = useMemo(() => {
return products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
}, [products, filter]);
// Stable reference for child components
const handleAddToCart = useCallback((product) => {
addToCart(product);
}, []);
return (
<ul>
{filteredProducts.map(product => (
<ProductItem
key={product.id}
product={product}
onAddToCart={handleAddToCart}
/>
))}
</ul>
);
};
Custom Hooks: Encapsulating Logic
Custom hooks are the secret to clean, reusable code:
useLocalStorage Hook
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// Usage
const [theme, setTheme] = useLocalStorage('theme', 'light');
useFetch Hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
if (err.name !== 'AbortError') {
setError(err);
setLoading(false);
}
});
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
Common Pitfalls and Solutions
1. Stale Closures
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
// ❌ Always logs 0
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, []); // Missing dependency
// ✅ Use functional update
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
}
2. Infinite Loops
// ❌ Infinite loop
useEffect(() => {
setState(someValue);
}, [someValue]);
// ✅ Use ref for values that shouldn't trigger updates
const someRef = useRef(someValue);
Performance Optimization Patterns
1. Memoizing Components
const ExpensiveComponent = React.memo(({ data, onUpdate }) => {
return (
<div>
{/* Expensive rendering */}
</div>
);
}, (prevProps, nextProps) => {
// Custom comparison logic
return prevProps.data.id === nextProps.data.id;
});
2. Virtual Lists for Large Datasets
import { useVirtual } from 'react-virtual';
function VirtualList({ items }) {
const parentRef = useRef();
const rowVirtualizer = useVirtual({
size: items.length,
parentRef,
estimateSize: () => 50,
});
return (
<div ref={parentRef} style={{ height: 400, overflow: 'auto' }}>
<div style={{ height: rowVirtualizer.totalSize }}>
{rowVirtualizer.virtualItems.map(virtualRow => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: virtualRow.size,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{items[virtualRow.index]}
</div>
))}
</div>
</div>
);
}
Conclusion
Mastering hooks requires understanding both the mechanics and the mental model. Remember:
- Keep hooks simple and focused
- Profile before optimizing
- Share logic through custom hooks
- Always clean up side effects
The best React code is often the simplest React code.
What's your favorite custom hook? Let me know in the comments!
Enjoyed this article?