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)