EmpireUI
Get Pro
← Blog8 min read#react hooks#useMemo#useCallback

useMemo vs useCallback: When to Use Each (and When to Skip Both)

useMemo and useCallback aren't free — they have overhead. Learn exactly when each hook earns its keep, and when you're just adding complexity for nothing.

Developer writing React code on a laptop with a dark editor theme

The Difference Nobody Explains Clearly

Both hooks memoize things. That's the sentence that trips most people up — because 'things' means something different in each case. useMemo memoizes a *value*. useCallback memoizes a *function*. Same underlying mechanism, different output type. That distinction matters a lot in practice.

Honestly, useCallback(fn, deps) is literally just shorthand for useMemo(() => fn, deps). React's own docs confirmed this back in 2023. So if you've ever wondered why they feel so similar, it's because they're nearly identical under the hood.

The reason both exist is developer ergonomics. Wrapping a function reference in useMemo reads weird — returning a function from another function every render feels off. useCallback is the cleaner surface API. That's really the whole story.

What useMemo Actually Does (With a Real Example)

useMemo caches the *result* of a computation between renders. If the dependencies haven't changed, React skips running the function and hands you the cached value. It's useful when you've got something genuinely expensive — think filtering a 10,000-item list, running a Fibonacci calculation, or building a derived data structure from props.

Here's a concrete case. Say you're rendering a component that receives a raw items array and needs to sort + filter it before display:

const filteredItems = useMemo(() => {
  return items
    .filter(item => item.active)
    .sort((a, b) => b.score - a.score);
}, [items]);

Without useMemo, that .filter().sort() chain runs on every single render — even if items hasn't changed and you just toggled a tooltip open. With it, you pay the computation cost once per items change. Worth noting: if items changes on every render anyway (e.g. you're building a new array inline in JSX), useMemo won't help you at all.

Quick aside: if your computation runs in under ~1ms and your component renders infrequently, the overhead of useMemo itself — storing deps, comparing them, managing the cache — can actually be slower than just recomputing. Don't reach for it by default.

What useCallback is Actually For

useCallback is almost always about referential equality. JavaScript recreates function objects on every render, which means a function you pass as a prop to a child is technically a *new* function each time, even if the logic is identical. That breaks React.memo.

// Without useCallback — child re-renders every time parent does
const handleClick = () => doSomething(id);

// With useCallback — stable reference, child only re-renders when id changes
const handleClick = useCallback(() => doSomething(id), [id]);

The pattern only pays off when three things are true simultaneously: the child component is wrapped in React.memo, the function is passed as a prop to that child, and the parent renders frequently enough that redundant child renders are actually hurting you. All three. If any one of those is missing, you're paying useCallback's overhead for nothing.

In practice, the most common legit use case is passing callbacks into virtualized lists — react-window, react-virtual, that sort of thing — where you've got hundreds of rows and even a single extra render per row gets expensive fast.

The Case for Skipping Both

Look, these hooks are misused constantly. Browse any production codebase from 2021-2024 and you'll find useCallback wrapped around *everything* — event handlers on divs, inline onClick props, functions that never leave the component. It's cargo-cult optimization.

React renders are fast. A typical functional component re-rendering takes single-digit milliseconds or less. The comparison React does for useMemo and useCallback deps isn't free — it's shallow equality on each dep in the array. If you've got a deps array with 6 items and your component renders 3 times a second, you're doing 18 equality checks per second just to *maybe* skip some work.

The real performance wins in React come from structural fixes: fewer state updates, pushing state down to smaller subtrees, colocating data, avoiding unnecessary context subscriptions. Those changes are free. Sprinkling useMemo everywhere without profiling first is a distraction.

That said, there are clear-cut cases where both hooks are exactly right. CPU-heavy derived data, stable callbacks for memoized children, dependencies for useEffect that you want to keep stable — these are real. Just profile before you optimize, not after.

Dependency Arrays: The Part That Bites You

Both hooks live or die by their dependency arrays. Get them wrong and you either get stale closures (deps too loose) or broken memoization (deps change every render). The eslint-plugin-react-hooks exhaustive-deps rule exists for a reason — run it.

The sneaky trap is objects and arrays as dependencies. If you pass options={{ size: 48 }} as a dep, that's a new object reference every render. Your useMemo will recompute every single time, making it completely pointless. You'd need to either memoize options itself, or destructure its primitive values into the deps array:

// Bad — options is a new object every render
const result = useMemo(() => compute(options), [options]);

// Better — depend on the primitive values directly
const { size, color } = options;
const result = useMemo(() => compute({ size, color }), [size, color]);

One more thing — if you find yourself adding a function to a useEffect deps array and it keeps triggering the effect, that's the classic signal to wrap that function in useCallback. It's not about performance at that point, it's about correctness.

A Decision Framework You Can Actually Use

Stop guessing. Here's the flowchart: Are you computing a derived value? Use useMemo — but only if that computation is genuinely expensive (>1ms, benchmark it) or if the value is used as a dependency elsewhere. Is the value a function? Use useCallback — but only if it's passed to a React.memo child or used as a useEffect dependency.

Neither of those? Delete the hook. Seriously. Your component will be easier to read, easier to test, and probably just as fast. React 19's compiler handles a lot of this automatically anyway — if you're on React 19+, a large chunk of manual memoization is already obsolete.

When you're building with design-heavy components — like the ones you'd pull from Empire UI — the component internals are already optimized. You don't need to wrap every callback in useCallback just because you're composing a lot of UI. Focus your memoization budget on data transformation layers, not render logic.

If you need to measure, reach for the React DevTools Profiler or the browser's Performance tab. Record a slow interaction, look at which components render and why, then fix the actual bottleneck. That 12px margin you're fussing over in your component tree isn't the problem. The problem is almost always an upstream state change triggering an entire subtree.

Quick Summary: The Rules

Use useMemo when: the computation is expensive, the result is used as a dep in useEffect or another hook, or you need referential stability for a non-function value like an array or object.

Use useCallback when: you're passing a function to a React.memo-wrapped child, the function is a useEffect dependency and you don't want the effect to re-run unnecessarily, or you're working with virtualized lists where render count matters.

Skip both when: you're just trying to 'be safe', your component renders rarely, you haven't profiled anything, or you're on React 19+ with the compiler enabled. Also worth browsing the gradient generator and box shadow generator if you're building performant, visually rich UIs — keeping expensive CSS computations out of JavaScript is its own kind of optimization. And if you're exploring component styles, the glassmorphism components on Empire UI are already built with this stuff in mind.

FAQ

Is useCallback just useMemo for functions?

Yes, exactly. useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). React exposes both because the latter reads awkwardly when you're just trying to memoize a function reference.

Does useMemo prevent re-renders?

No — it prevents expensive recomputations, not re-renders. To skip re-renders, you need React.memo on the child component. useMemo only affects the work done inside your component, not whether the component itself runs.

Should I use useCallback for every event handler?

No. Only wrap event handlers in useCallback if they're passed as props to a React.memo child or used as a useEffect dependency. Otherwise you're adding overhead for nothing.

Does React 19 make these hooks obsolete?

Mostly, yes — React 19's compiler auto-memoizes a lot of what you'd do manually. That said, it doesn't cover every case, so understanding the underlying concepts still matters.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

React.memo, useMemo and useCallback: The Honest Guide to When You Need ThemReact.memo vs useMemo vs useCallback: A Practical Decision GuideLottie Animations in React: Setup, Optimisation and PitfallsPage Load Animation in React: First Paint, Stagger, Skeleton to Content