
If you’ve spent any real time in React, you know the reflex. You spot a chunky computation, and your fingers type useMemo before your brain finishes the thought. A callback heads to a child component, and useCallback wraps it like shrink-wrap. The docs and a hundred Medium posts told you these hooks stop pointless re-renders. But after profiling real, living applications for years, I’ve hit a less comfortable truth: slapping useMemo and useCallback everywhere often backfires. Hard.
I’m Suki Watanabe. My days are spent inside a large-scale SaaS, hunting actual performance regressions. I used to reach for these hooks like salt at dinner—automatic, no tasting first. The outcome? Codebases that got harder to read, trickier to maintain, and sometimes measurably slower than before. This isn’t a theory piece. It’s a field report on when these hooks earn their keep, and when you should step back and let React breathe.
The Premature Optimization Trap
React’s own docs warn against optimizing too early. Still, a kind of folklore has spread that useCallback and useMemo are always a good idea. They aren’t. These hooks were built for two specific headaches: referential equality and genuinely expensive calculations. If your component doesn’t ache from either one, you’re just adding clutter.
Take a plain presentational component that renders a list. That list array gets defined inside the parent and passed down. A newer developer might wrap it in useMemo, convinced they’re dodging a re-render. Trouble is, if the array changes on every render anyway—because it’s built from props that update constantly—the memoization adds cost with zero benefit. React now has to check the dependency array, diff old and new values, and hold onto the previous result. That work isn’t free.
“The overhead of useMemo and useCallback is real. You’re swapping plain JavaScript computation for memory and comparison work. Be sure you’re coming out ahead.”
I’ve walked into codebases where every single function and object wore a memoization jacket. The team felt productive. The app felt swampy. Why? They skipped the cardinal rule: measure first, optimize later.
Understanding Referential Equality
To use these hooks well, you need a working feel for referential equality in JavaScript. In React, a component re-renders when state or props change. Props get compared with a shallow check. Two objects that look the same but sit at different memory addresses will still spark a re-render. That’s where useMemo and useCallback earn their reputation—they keep the same reference across renders, provided the dependencies hold steady.

But that reference stability only matters if the child is wrapped in React.memo. Without it, the child blithely re-renders no matter what. I’ve debugged too many components where useCallback was applied like sunscreen, yet the child wasn’t memoized. The hooks just sat there, burning memory for nothing.
Here’s a real scenario. Picture a SearchBar that takes an onChange prop. If the parent defines that handler inline, every render of the parent spawns a new function reference. If SearchBar is wrapped in React.memo, it’ll still re-render because that reference changed. useCallback fixes that—provided SearchBar is memoized. Miss that combo, and you’re just adding brackets for exercise.
The Hidden Cost of Dependencies
Every useCallback and useMemo carries a dependency array. Managing those arrays breeds subtle bugs. Omit a dependency and you get a stale closure; pile in too many and the cache invalidates constantly, erasing the point. ESLint’s react-hooks/exhaustive-deps rule nudges you in the right direction, but it’s not a cure-all. I’ve watched developers silence the rule with a comment because they “know better.” They usually don’t.
A stale closure can be nasty. Your callback clings to an old value, and the UI drifts into weird, unreproducible states. This gets especially dangerous inside event handlers or effects that depend on fresh state. The hours you burn chasing stale closures can easily swamp any performance win you thought you were getting.
When useMemo Actually Bites Back
Let’s talk about useMemo for “expensive” calculations. The textbook example is filtering a big list. If the list and filter terms rarely change, memoizing the filtered result makes sense. But what if that list churns on every render? Now the memoization piles on overhead each time, and the cache gets tossed almost instantly. Net loss.
I once profiled a dashboard that ate real-time data. A developer had memoized a derived data structure that leaned on five different state variables. The dependency array was a monster, and the computation itself wasn’t heavy—some sorting, a few maps. Stripping the useMemo shrunk the component’s memory footprint and made interactions snappier. The lesson: don’t guess a calculation is slow. Measure it.

Another trap is wrapping inline styles or class name strings in useMemo. Building a tiny object or concatenating a few strings is dirt cheap. useMemo just muddies the code and forces the next reader to ask, “Why is this cached?” Usually the answer is: “Because someone thought it looked faster.” If the object’s values come from props, React’s reconciliation already has your back. Trust the framework until it lets you down.
Server-Side Rendering and Hydration
In server-rendered React apps, useMemo and useCallback can tangle hydration. The server renders once, so memoization doesn’t help there. During hydration, React has to match the server output exactly. If your hooks have side effects or touch browser-only APIs, mismatches creep in. I’ve lost evenings to hydration errors that traced back to a useMemo that calculated something differently on the client.
This doesn’t mean you should rip them out of SSR apps. It means you need precision. Understand what runs where. A pure calculation that only touches props is fine. Anything that peeks at window or localStorage belongs in an effect, not a memo hook.
The Readability Factor
Code gets read way more than it gets written. Overdosing on useMemo and useCallback chews up readability. A component laced with these hooks forces you to mentally chase dependencies and guess what’s cached and why. Often the reason is just “the last dev thought it would be faster.”
I stick to a straightforward rule: useCallback only when you pass the function to a memoized child or into a dependency array of an effect. useMemo only when the computation is demonstrably slow (you’ve profiled it) and its inputs change rarely. For everything else, let React’s defaults run. The framework is fast enough for the vast majority of what we build.
Look at this snippet:
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
If handleClick just sits on a plain button, the useCallback is dead weight. Even when the parent re-renders, creating a fresh function is cheaper than the bookkeeping useCallback adds. The React team has said this out loud, more than once. Yet the pattern still shows up everywhere.
Practical Guidelines from the Trenches
After years of pulling React apps apart and putting them back together, I lean on a few heuristics that keep things fast and sane.
- Profile before wrapping. Fire up the React DevTools Profiler and find real choke points. Look for components that re-render often and take a long time per render. Then think about memoization.
- Memoize children, not every prop. If a component is genuinely heavy to render, wrap it in
React.memo. Then use useCallback and useMemo for the props that need referential stability. - Avoid memoizing primitives. Strings, numbers, booleans—these compare by value. useMemo on them adds zero value.
- Keep dependency arrays small. If your useCallback depends on five state variables, it’s probably doing too much. Pull out a smaller function or reshape the component.
- Consider alternative patterns. Sometimes moving state down or lifting content up removes the need for memoization entirely. The upcoming React compiler aims to automate a lot of this, making manual hooks less necessary.
These aren’t rigid laws. They’re starting points. Every app has its own performance fingerprint. Stay curious, stay skeptical. When someone tells you to “just use useMemo everywhere,” ask to see the flame chart.
FAQ: Common Doubts About useCallback and useMemo
Should I wrap every function in useCallback if I use it in a useEffect?
Not automatically. If the function lives inside the component and is used only in that effect, try moving the function into the effect itself. That cuts out the need for memoization and makes the dependency chain obvious. Reach for useCallback only when the function is needed elsewhere—like in a child component or another hook.
Isn’t it always safer to memoize just in case?
No, it’s not safer. Unnecessary memoization adds mental noise, nudges bundle size up slightly, and can introduce stale closure bugs when dependencies slip. React’s default rendering is fast. The “safety” you feel is mostly an illusion that leads to knottier, harder-to-debug code.
How do I know if a calculation is “expensive” enough for useMemo?
Profile it. Wrap the calculation with console.time, or better, use the React Profiler. If it consistently runs above 1ms and the component renders often, useMemo might earn its keep. For most sorting, filtering, or object creation on small datasets, it won’t matter. Always test with realistic data sizes under production-like conditions.
In the end, useCallback and useMemo are tools, not rituals. They fix specific problems in specific spots. The strongest React developers I know deploy them sparingly, deliberately. They trust React’s internals and only step in when the profiler leaves them no choice. Next time you’re about to wrap that function, pause and ask: what am I actually gaining? Your future self—and your teammates—will be glad you did.