If you’ve written React for more than a week, you’ve used these hooks. The problem isn’t syntax, it’s knowing when each one matters and when you’re just adding complexity for no reason.
I’ve gone through phases with hooks. First I overused everything. Then I stopped using useMemo and useCallback entirely because “premature optimization.” Now I’m somewhere in the middle, reaching for each hook when it actually solves a problem. That’s what this post is, a practical cheat sheet for four hooks I use daily, with the gotchas that caught me off guard.
useState: Your Component’s Memory
The basic building block. You need state, you use useState.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div className="counter-container">
<h3>Count: {count}</h3>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
The Functional Update Thing
This one matters more than you’d think. If your new state depends on the current state, always use the functional form:
setCount(prevCount => prevCount + 1);
Without it, you can get stale values in closures, especially in event handlers or intervals that capture the state value at render time. I had a polling component that was incrementing a counter but occasionally skipping numbers. The fix was exactly this.
useEffect: The Side Effect Swiss Army Knife
Anything that happens “outside” the component render cycle, data fetching, subscriptions, DOM manipulation, timers, goes here.
The Dependency Array
This is where most bugs live. Seriously.
// Runs after every render (almost never what you want)
useEffect(() => {
const fetchData = async () => {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
};
fetchData();
}, []); // Empty array means this runs once on mount
- No array: Runs after every render. You almost never want this.
- Empty
[]: Runs once on mount. Good for initial data fetches. - With dependencies
[a, b]: Runs whenaorbchanges. Put everything your effect actually uses in here.
Clean Up After Yourself
If your effect sets up a subscription or timer, return a cleanup function:
useEffect(() => {
const timer = setInterval(() => {
console.log('Tick');
}, 1000);
return () => clearInterval(timer); // Cleanup on unmount
}, []);
I shipped a component once that added a scroll listener without cleanup. Every time the component remounted, it stacked another listener. The app got progressively slower until someone noticed the tab was eating memory like crazy. Clean up your effects.
useMemo: Cache the Expensive Stuff
useMemo caches the result of a computation. It only recalculates when its dependencies change.
const expensiveCalculation = (num) => {
console.log('Calculating...');
for (let i = 0; i < 1000000000; i++) {} // Artificial delay
return num * 2;
};
function App({ number }) {
const memoizedValue = useMemo(() => expensiveCalculation(number), [number]);
return <div>Result: {memoizedValue}</div>;
}
When to Actually Use It
Two cases where it’s genuinely worth it:
- Expensive calculations: anything that takes noticeable time. Filtering a massive array, complex math, that kind of thing.
- Object reference stability: when you’re creating an object or array that’s passed to a memoized child component. Without useMemo, the reference changes every render and the child re-renders anyway.
When NOT to Use It
Don’t wrap every calculation in useMemo. The hook itself has overhead, it stores the result and checks dependencies on every render. For cheap operations, you’re adding complexity for zero benefit. I’ve seen code where useMemo(() => value + 1, [value]) was a thing. Don’t do that.
useCallback: Stable Function References
While useMemo caches a value, useCallback caches a function.
Functions get recreated on every render in JavaScript. That’s just how it works. If you pass a function as a prop to a child component wrapped in React.memo, that child re-renders every time because the function reference changed, even if the function does the exact same thing.
const handleIncrement = useCallback(() => {
setCount(c => c + 1);
}, []); // Function reference stays the same across renders
The Real Use Case
function Parent() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);
// Without useCallback, MemoizedButton re-renders every time Parent renders
const handleClick = useCallback(() => {
console.log('Count:', count);
}, [count]);
return (
<>
<MemoizedButton onClick={handleClick} />
<button onClick={() => setOther(other + 1)}>Other: {other}</button>
</>
);
}
Changing other won’t cause MemoizedButton to re-render because the handleClick reference is stable.
The Honest Take
useCallback is the most overused optimization hook. If the child component isn’t memoized, wrapping the callback in useCallback does literally nothing. And even when it is memoized, the performance difference is usually negligible unless you’re rendering hundreds of items or dealing with very expensive child renders.
Use it when you have a measurable problem. Not by default.
Quick Reference
| Hook | Best Used For | Key Behavior |
|---|---|---|
| useState | Local state | Triggers a re-render when updated. |
| useEffect | Side effects | Runs after render; supports cleanup. |
| useMemo | Calculated values | Returns a cached value. |
| useCallback | Functions | Returns a stable function reference. |
The Actual Advice
Here’s what I’d tell myself when I was starting with hooks:
- Start with useState and useEffect. They cover 90% of cases.
- Add useMemo when you see a performance problem with expensive calculations or when referential equality matters for memoized children.
- Add useCallback when the same. If a child is memoized and re-renders anyway because of function references, wrap the callback.
- Don’t put
eslint-plugin-react-hookson ignore. The exhaustive-deps rule is annoying but it catches real bugs. - Profile before optimizing. React DevTools Profiler is your friend. Don’t guess.
The best React code isn’t the code that uses every optimization hook, it’s the code that’s simple enough that it doesn’t need them.
Member discussion
0 commentsStart the conversation
Become a member of >hacksubset_ to start commenting.
Already a member? Sign in