๐ฅ 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)