React gives us some amazing hooks like useCallback, useMemo, and useReducer. They’re powerful, but here’s the catch: using them in the wrong place can actually make your app slower and harder to maintain.
In this post, I’ll break down:
- What each hook does
- When you should use it
- When you shouldn’t use it (common anti-patterns)
- Better real-world examples
🔹 useCallback
✅ What it does
Keeps a function reference stable between renders. Useful when passing callbacks to memoized child components.
❌ When NOT to use it
On simple inline functions (onClick={() => setOpen(true)})
When you’re not passing the function to a child component
If the child isn’t wrapped in React.memo
✅ Good example
import { useCallback, useState, memo } from "react";
const Button = memo(({ onClick }) => {
console.log("Button rendered");
return <button onClick={onClick}>Click Me</button>;
});
export default function App() {
const [count, setCount] = useState(0);
// ✅ useCallback prevents function recreation on each render
const handleClick = useCallback(() => {
console.log("Clicked!");
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<Button onClick={handleClick} />
</div>
);
}
Without useCallback, the would re-render every time count changes.
🔹 useMemo
✅ What it does
Memoizes the result of a calculation so it’s only recomputed when dependencies change.
❌ When NOT to use it
- For cheap operations (a + b)
- For small lists or trivial filtering/sorting
- Just “because” (it adds overhead itself) ✅ Good example
import { useMemo, useState } from "react";
export default function App() {
const [filter, setFilter] = useState("");
const [items] = useState(
Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`)
);
// ✅ Expensive filtering only runs when "filter" changes
const filteredItems = useMemo(() => {
console.log("Filtering...");
return items.filter((item) => item.includes(filter));
}, [filter, items]);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Search..."
/>
<ul>
{filteredItems.slice(0, 10).map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
If you filter a huge list without useMemo, React would recompute the filter on every keystroke, even if unrelated state changes.
🔹 useReducer
✅ What it does
Alternative to useState for managing complex state with multiple transitions.
❌ When NOT to use it
- When state is simple (isOpen, count, inputValue)
- When you don’t need multiple actions or a reducer function
- For small components where useState is enough ✅ Good example
import { useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "add":
return { ...state, todos: [...state.todos, action.payload] };
case "remove":
return {
...state,
todos: state.todos.filter((t, i) => i !== action.index),
};
default:
return state;
}
}
export default function App() {
const [state, dispatch] = useReducer(reducer, { todos: [] });
return (
<div>
<button onClick={() => dispatch({ type: "add", payload: "Learn Hooks" })}>
Add Todo
</button>
<ul>
{state.todos.map((todo, i) => (
<li key={i}>
{todo}{" "}
<button onClick={() => dispatch({ type: "remove", index: i })}>
❌
</button>
</li>
))}
</ul>
</div>
);
}
For complex state transitions like a todo list, shopping cart, or form wizard, useReducer shines. For a simple counter? useState is enough.
💡What about you? Have you ever overused these hooks and then realized they were unnecessary? Share your experience in the comments!
Top comments (0)