🔥 Your React App is Secretly Slowing Down – Here’s How to Fix It With Just One Hook!
React is loved for its simplicity, declarative style, and component-based architecture. But beneath the surface of even the cleanest codebases lies a haunting truth – re-renders are silently sabotaging your performance.
This post is not another basic "Use React.memo!" kind of article. Instead, we're diving deep into a lesser-used yet incredibly powerful hook that can magically save your app from performance death: useCallback — and more importantly, how and when to use it correctly.
In this post, you'll learn:
- 👉 Why your app is slowing down despite using React.memo
- 👉 What really triggers re-renders
- 👉 What useCallback solves (and what it doesn't)
- 👉 A step-by-step code example translating laggy UI into buttery smooth UX
- 👉 A custom hook trick to analyze what components are re-rendering — like a profiler!
😱 The Hidden Performance Problem
Let’s say you have a parent component passing a function to a child.
function Parent() { const [count, setCount] = useState(0); const handleClick = () => { console.log("Clicked!"); }; return ( <div> <button onClick={() => setCount(c => c + 1)}>Increment</button> <MemoizedChild onClick={handleClick} /> </div> ); } const MemoizedChild = React.memo(({ onClick }) => { console.log("Child rendered"); return <button onClick={onClick}>Click Me</button>; }); You'd expect MemoizedChild to not re-render when count changes, right? WRONG.
😬 Why?
Because handleClick is re-created on every render. For React.memo, new function === new prop, so the memoized component re-renders.
✅ Enter useCallback
const handleClick = useCallback(() => { console.log("Clicked!"); }, []); Now, handleClick has a stable reference until the dependencies change (empty in this case), so MemoizedChild doesn’t re-render unnecessarily.
Let’s verify it step by step.
🕵️♂️ Create a Render Visualizer to Spot Unwanted Renders
A neat trick to help debug performance:
function useRenderTracker(name) { const renders = useRef(0); useEffect(() => { renders.current++; console.log(`${name} rendered ${renders.current} times`); }); } function Parent() { useRenderTracker("Parent"); // ... } const MemoizedChild = React.memo(function Child({ onClick }) { useRenderTracker("Child"); return <button onClick={onClick}>Click</button>; }); Nothing like real-time logs to show the hidden performance creepers. Run both versions (with and without useCallback) and observe the difference in renders.
❌ But Don’t Overuse useCallback
Now, before you go and wrap every function call inside useCallback, hold up!
⚠️ Common pitfalls:
- It adds complexity
- Recreating the callback can often be cheaper than memoizing
- If the function isn’t passed to a memoized child component or used in a dependency array, it’s likely unnecessary
🔑 Rule of thumb:
Use useCallback only when passing callbacks to memoized components or using them inside useEffect/useMemo dependency arrays.
⚙️ A Real-World Optimization: Dynamic Search UI
React apps often suffer from performance hits when passing state-updating functions to children “search boxes,” especially during typing.
Here’s an optimized example:
function SearchForm({ onChange }) { return <input onChange={e => onChange(e.target.value)} />; } const MemoizedSearchForm = React.memo(SearchForm); function App() { const [query, setQuery] = useState(""); const handleSearch = useCallback((value) => { setQuery(value); }, []); return ( <div> <MemoizedSearchForm onChange={handleSearch} /> <ResultsList query={query} /> </div> ); } Without useCallback, unnecessary re-renders might make your search feel slower.
💡 Bonus: Babel Plugin to Track Anonymous Functions
To really hammer down these problems, there are tools like eslint-plugin-react-perf or even custom Babel transforms that warn you when you're passing anonymous functions as props.
🧠 Final Thoughts
Most performance pain in medium-large React apps comes from unintended re-renders, often due to unstable function references. This is one of the less intuitive performance bugs because the UI looks fine — until it doesn’t.
🚀 Learn to:
- Use useCallback sparingly but purposefully
- Understand when child components really need to re-render
- Leverage React.memo AND stable props
This small shift in mindset can drastically improve the feel of your app — and your users will feel it too.
Happy coding!
🔗 Further Reading
- React Performance Optimization Docs
- Kent C. Dodds – AHA Programming
- Why React.memo is broken if you don’t know this
👉 If you need professional help optimizing your React frontend for performance, we offer frontend development services.
Top comments (0)