Prevent Re-Renders in React Memo with useCallback
Stop unnecessary re-renders in React.memo child components by using useCallback for stable callback props. Learn fixes, downsides, profiling, and best practices for React optimization.
How can I prevent unnecessary re-renders when passing callbacks to memoized child components in React?
I’m working on a React application where a parent manages state and passes a callback to a memoized child (React.memo). Even though the props used inside the child don’t change, the child still re-renders when the parent state updates.
Example:
import React, { useState, useCallback } from "react";
const Child = React.memo(({ onClick }) => {
console.log("Child rendered");
return <button onClick={onClick}>Click</button>;
});
export default function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log("Button clicked");
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<Child onClick={handleClick} />
</div>
);
}
I understand that functions are recreated on every render, which can change the prop reference and trigger re-renders. My questions:
- Is wrapping
handleClickwithuseCallbackthe correct and recommended approach to prevent the child from re-rendering? - Are there any downsides to overusing
useCallback(e.g., memory/CPU overhead, added complexity)? - In real-world applications, how do you decide when memoization (
React.memo/useCallback) is actually worth it versus being premature optimization? What are best practices and heuristics (profiling, measuring, component complexity) for deciding when to memoize?
Yes, wrapping your handleClick callback with useCallback is the recommended fix to prevent unnecessary re-renders in React.memo child components— it keeps the function reference stable across parent renders, so shallow prop comparison passes. Without it, every parent update creates a fresh function, fooling React.memo into thinking props changed. Official React docs back this as the go-to combo for React usecallback and memoization, but profile first to confirm it’s worth the tweak.
Contents
- Why Your Memoized Child Still Re-renders
- Fixing It: React.memo with useCallback
- Downsides of Overusing useCallback
- When Memoization Is Worth It in Real Apps
- Profiling and Measuring Re-renders
- Best Practices and Heuristics
- Sources
- Conclusion
Why Your Memoized Child Still Re-renders
You’ve spotted the classic gotcha. In your example, the Child logs “Child rendered” every time you increment count. Why? React.memo only skips re-renders if props are shallowly equal—using Object.is under the hood. Primitives like numbers or strings compare fine, but functions? They get recreated on every parent render.
Think about it. Your handleClick is defined inline in the component body. Parent re-renders (from setCount), boom—new function object. Reference changes, React.memo bails, child paints again. Even if the button’s logic hasn’t budged.
This hits harder in lists or dashboards. Imagine 50 Child buttons. Parent ticks? 50 pointless re-renders. Wasteful, right? But not every case needs fixing. Simple buttons like yours? Maybe overkill. The real pain shows in heavy components—charts recalculating, virtualized lists rebuilding.
From the official React docs on memo, this shallow check is deliberate: fast, predictable. No deep diffs (too slow). So callbacks demand special care.
Fixing It: React.memo with useCallback
Enter useCallback. Wrap that function, and it memos the reference based on deps. Here’s your code, patched:
import React, { useState, useCallback } from "react";
const Child = React.memo(({ onClick }) => {
console.log("Child rendered");
return <button onClick={onClick}>Click me</button>;
});
export default function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("Button clicked");
}, []); // Empty deps: stable forever
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<Child onClick={handleClick} />
</div>
);
}
Now? Child only renders once. Magic? Nah, stability. The React useCallback reference calls this “the recommended way” for memoized kids. Deps array controls freshness—if empty [], it’s eternal (great for no-dependency loggers). Add count? Updates when count does.
But wait, your increment button still uses inline () => setCount(count + 1). That’s a stale closure trap! Fix with updater: onClick={() => setCount(c => c + 1)}. No deps needed anywhere.
Pro tip: Pair with React.memo only on children that cost to render. Your button? Pennies. A canvas chart? Dollars.
Downsides of Overusing useCallback
useCallback isn’t free lunch. It trades re-renders for other costs. First, memory: Each call allocates a closure, stashed in a cache. Tiny per hook (bytes), but scale to 100 components? Accumulates. Closures capture scope—hold big objects? GC delays, leaks possible.
CPU hit? Dep array checks every render. ESLint nags missing deps, forcing exhaustive lists. Miss one? Stale callbacks (hello, bugs). Inline arrows? Still create functions—just cache 'em.
Complexity spikes too. Code bloats: useCallback(fn, [dep1, dep2]). Readability tanks. Kent C. Dodds breaks it down: “Optimizations cost—measure the win.” Overdo it everywhere? Slower and uglier.
In practice? Your empty-deps case is safe. But deps like state/objects? Recreate often, negating benefits. LogRocket nails it: Minor overhead for non-memoized props—wasted.
Bottom line: Use surgically. Not “because functions are bad.”
When Memoization Is Worth It in Real Apps
Real-world React? 90% don’t need this until users complain. Premature React optimization kills velocity. But when lists lag or keyboards stutter? Time to profile.
Heuristics from pros:
- Parent volatility: Re-renders often (timers, inputs, effects)? Memoize volatile props to kids.
- Child expense: Render >1-5ms? Memoize. DevTools Profiler flags it. Simple
<div>? Skip. - Cascade risk: One parent tick ripples to 10+ kids? Yes. React docs advise: “Profile before adding.”
- Scale signals: 100+ instances, virtualized lists, charts (Recharts/D3). Your button? Nah.
In dashboards I’ve built, memoizing row renderers in a 1000-line table saved 200ms/frame. But a navbar link? Crickets.
React 19’s compiler auto-memos stable stuff—less manual work soon. Until then, heuristic: If “why-did-you-render” lib screams on prod data, act.
Profiling and Measuring Re-renders
Don’t guess—measure. React DevTools Profiler: Record a session, increment count 5x. Flamegraph shows child highlights? Red flag. Commit count >1? Unnecessary.
Chrome Performance tab: React hooks layer reveals render trees. “why-did-you-render” npm package: Logs prop diffs in dev. Brutal truth serum.
Benchmarks: Time 100 renders with/without. >10% delta? Worth it. Tech blogs like LogRocket demo this: Child render at 2ms x 50 = 100ms waste.
Heuristic thresholds:
- Child render <1ms: Ignore.
- Parent re-renders >10/sec: Memoize.
- Total cascade >50ms/frame: Prioritize.
Tools make it data-driven. No vibes.
Best Practices and Heuristics
Start simple:
- Local state first: Keep logic in child. No prop drill.
- Functional updaters:
setState(prev => ...)—zero deps. - Primitives over objects: Pass
count, not{count}. - Profile → Stabilize → Measure: Cycle.
- Custom equality:
React.memo(Child, (prev, next) => prev.id === next.id)for deep cases.
From KentCDodds: Focus pure components, avoid lifts. Official guidance: “Only for memoized props or deps.”
In teams? Linting: Enforce exhaustive deps, warn overuse. Real apps: Dashboards/memos win; forms/tooltips lose.
And yeah, overuse bloats. But done right? Smooth 60fps.
Sources
- useCallback – React
- memo – React
- React useCallback: When and how to use it for better performance - LogRocket Blog
- React.memo explained: When to use it (and when not to) - LogRocket Blog
- When to useMemo and useCallback - Kent C. Dodds
Conclusion
React.memo paired with useCallback nails unnecessary re-renders from callback props—stable refs win the day. But downsides like memory and complexity mean profile religiously: DevTools, heuristics (expensive kids, volatile parents), and measure deltas. Best path? Build fast, optimize slow—your app will thank you with buttery performance where it counts.