🔥 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:
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.
Because handleClick is re-created on every render. For React.memo, new function === new prop, so the memoized component re-renders.
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.
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.
Now, before you go and wrap every function call inside useCallback, hold up!
🔑 Rule of thumb:
Use useCallback only when passing callbacks to memoized components or using them inside useEffect/useMemo dependency arrays.
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.
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.
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:
This small shift in mindset can drastically improve the feel of your app — and your users will feel it too.
Happy coding!
👉 If you need professional help optimizing your React frontend for performance, we offer frontend development services.
Information