🔥 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)