SassTurf
BlogBuilding
Building

React useCallback: A Founder's Guide to Not Overusing It (2026)

React useCallback explained without the jargon — what it does, when it actually helps, and why you're probably overusing it (React Compiler changes things).

Shubham Soni
Shubham Soni
Jul 13, 2026 · 9 min read
Table of contents6 sections
  1. 01The PR Comment That Sent Me Down Another Rabbit Hole
  2. 02What useCallback Actually Does (Not What You Think)
  3. 03The One Case Where It Genuinely Matters
  4. 04Why You’re Probably Overusing It
  5. 05The 2026 Plot Twist: React Compiler Does This For You
  6. 06What To Actually Do (My Broke-Engineer Checklist)

The PR Comment That Sent Me Down Another Rabbit Hole

In 2023, while I was busy over-engineering Clickly — my Bitly-style URL shortener that I’ve written a small library of confessions about — I hired a part-time frontend guy to help me move faster. Faster. That’s funny in hindsight.

He opened his first pull request, and I still remember the diff. Every single event handler in the dashboard was wrapped in useCallback. Every one. The “copy link” button. The “delete” button. A handler that literally just did console.log. It was useCallback all the way down, like the code equivalent of putting a lock on your own fridge.

I asked him why. His answer was the answer I’ve heard a hundred times since: “For performance.”

That was the moment I realized most React developers — including me, back then — treat useCallback like a vitamin. Sprinkle it everywhere, can’t hurt, might help. Except it can hurt, it rarely helps, and in 2026 the whole ritual is quietly becoming obsolete. So let me save you the three weeks I spent actually understanding this.


What useCallback Actually Does (Not What You Think)

Here’s the thing that trips everyone up: useCallback does not make your function faster. It doesn’t optimize the code inside it. It doesn’t cache the result of the function. (Chasing real speed instead? React performance optimization covers the wins that actually move the needle.)

What it does is much smaller and much more specific. It caches the function reference itself across re-renders.

In JavaScript, every time a component re-renders, any function you declare inside it is created fresh — a brand new object in memory. It behaves identically, but to the computer it’s a different function. Same recipe, different physical piece of paper.

function LinkRow({ url }) {
  // A brand new function object on EVERY render
  const handleCopy = () => navigator.clipboard.writeText(url);
  return <button onClick={handleCopy}>Copy</button>;
}

Wrap it in useCallback and React hands you back the same function object between renders — until one of its dependencies changes:

function LinkRow({ url }) {
  // Same function object across renders, until `url` changes
  const handleCopy = useCallback(
    () => navigator.clipboard.writeText(url),
    [url]
  );
  return <button onClick={handleCopy}>Copy</button>;
}

That’s it. That’s the whole feature. It stabilizes an identity, not a speed. (useMemo is its sibling — it caches a computed value instead of a function; a plain useMemo returning a function is basically useCallback with more typing.)

So the obvious question is: who cares whether the function is the same object? For a plain onClick, nobody. The browser doesn’t care. React doesn’t care. Attaching a fresh function to a button 60 times a second costs you nothing measurable. My frontend guy’s console.log handler wrapped in useCallback was pure ceremony.


The One Case Where It Genuinely Matters

There is exactly one situation where that stable identity earns its keep, and it comes in two flavors of the same idea: something downstream compares the reference.

Flavor 1: You pass it to a component wrapped in memo

React.memo wraps a child component and tells React, “don’t re-render this unless its props actually changed.” But “changed” here means a shallow comparison — and for a function prop, shallow comparison is just oldFn === newFn. If the parent re-renders and hands the child a brand new function object every time, memo sees a “new” prop, and re-renders anyway. Your memoization is defeated silently. You did the work and got nothing.

const Child = memo(function Child({ onAction }) {
  // Expensive to render; memo is supposed to protect it
  return <HeavyChart onAction={onAction} />;
});

function Parent() {
  // Without useCallback, `onAction` is new every render,
  // so memo(Child) re-renders every render anyway — pointless.
  const onAction = useCallback(() => doThing(), []);
  return <Child onAction={onAction} />;
}

That is when useCallback does real work: it keeps the reference stable so the memo wrapper can actually skip the re-render. No memo on the child? Then useCallback on the prop is doing nothing but burning a tiny slice of memory and your reader’s patience.

Flavor 2: The function is a dependency of another hook

The other legitimate case: you use the function inside a useEffect, useMemo, or another useCallback’s dependency array. If the function identity changes every render, the effect re-runs every render — which can mean re-subscribing to a socket, re-firing an API call, or in the worst case an infinite loop.

const fetchData = useCallback(() => api.get(id), [id]);

useEffect(() => {
  fetchData();
}, [fetchData]); // stable fetchData = effect runs only when `id` changes

Without the useCallback, fetchData is new every render, the effect sees a changed dependency, and it fires on every render. Now the stability is load-bearing.

Notice the pattern in both: useCallback is never about the function itself. It’s about protecting something that checks the function’s reference. No memoized child, no dependency array watching it? Then you almost certainly don’t need it. That’s the rule my hire didn’t know, and honestly that I didn’t fully internalize until I’d shipped the bug where an effect looped and hammered Clickly’s Neon database in a tight retry storm. Nothing teaches you dependency arrays like watching your own free-tier connection count spike at 2 AM.


Why You’re Probably Overusing It

Here’s the uncomfortable truth, and it’s the whole reason I’m writing this: the default advice online is wrong for you.

Somewhere along the way, “use useCallback when reference identity matters” mutated into “wrap every handler in useCallback to be safe.” It’s cargo-cult programming — copying the ritual without the reason. And it has real costs that nobody mentions:

  • useCallback is not free. It runs on every render too. React has to store the dependency array, compare it, and decide whether to return the cached function. For a handler that doesn’t feed a memo or an effect, you’ve added work to avoid work that was already free. Net negative.
  • It clutters the code. Every wrapped function is a dependency array you now have to maintain correctly. Miss a dependency and you get a stale closure — a bug that captures an old value and quietly serves wrong data. I’ve chased those for hours. They’re miserable.
  • It creates false confidence. People wrap handlers in useCallback, feel like they “optimized,” and never actually profile whether anything was slow. Meanwhile the real bottleneck — a 3 MB unoptimized image, a waterfall of API calls, an unindexed query — sits there untouched.

You know what actually made Clickly’s dashboard feel fast? Not useCallback. It was moving analytics reads off Postgres and paginating a list that was rendering 5,000 rows at once. The React memoization was rearranging deck chairs. Measure before you memoize. Open the React DevTools Profiler, find the component that’s actually re-rendering expensively, and fix that. Ninety percent of the time it’s not a function reference.

Premature optimization in React looks productive. It feels like engineering. It’s usually just noise with a dependency array attached.


The 2026 Plot Twist: React Compiler Does This For You

Here’s the part that makes half of this debate moot, and it’s the biggest reason I finally stopped arguing with people about it.

In October 2025, the React team shipped React Compiler 1.0 as stable. This isn’t a runtime library — it’s a build-time compiler that reads your components and automatically inserts memoization for you, based on static analysis. It figures out which values and functions can be safely cached, and does it — often more precisely than a human would, because it can even memoize in spots where you can’t manually put a hook (like after an early return). (I wrote a whole founder’s guide to the React Compiler — what it is, how to turn it on, and whether you should.)

The React team’s own words are worth quoting: the compiler memoizes based on its analysis, and “in most cases, this memoization will be as precise, or moreso, than what you may have written.” Meta ran it in production on the Quest Store and reported initial loads and cross-page navigations up to 12% faster, some interactions more than 2.5× faster, with memory usage staying flat.

It’s compatible with React 17 and up (it shines on React 19), Next.js 16 ships stable built-in support, and Expo turns it on by default in new projects. This is not a lab experiment anymore. Meta runs it across their apps.

What does that mean for you and your handlers? You write plain functions and let the compiler decide what to memoize. No useCallback, no useMemo, no memo sprinkled by hand. The console.log handler stays a console.log handler.

Now, the honest caveat, because I refuse to oversell: the React team is explicit that useCallback and useMemo are not deprecated. They remain escape hatches for when you need precise, manual control the compiler can’t infer. And if you’re on an older codebase without the compiler enabled, the old rules still apply — the one legitimate case above is still your guide. But for anything new, in 2026, the direction is unambiguous: stop hand-memoizing and let the toolchain do it. The lint rules that used to nag you now ship in eslint-plugin-react-hooks, and you don’t even need the compiler installed to benefit from them.


What To Actually Do (My Broke-Engineer Checklist)

Strip it down to what matters when you’re a solo founder who should be building features, not fondling render performance:

  1. Starting fresh in 2026? Turn on React Compiler and basically forget useCallback exists. Write normal functions. This is the move.
  2. On an older app without the compiler? Only reach for useCallback when you’re passing a function to a memo’d child, or it’s a dependency of a useEffect/useMemo. Otherwise, delete it.
  3. Think something’s slow? Profile it with the React DevTools Profiler before touching a single hook. The culprit is almost never a function reference.
  4. Never wrap a handler “to be safe.” Safe is a myth here. You’re adding cost and a stale-closure risk to prevent a cost that didn’t exist.

The deeper lesson is the same one that runs through everything I write: I burned real time over-engineering things that didn’t move the needle, on a product with zero users. useCallback on every button was the frontend version of me benchmarking seven databases for a URL shortener nobody was clicking. (If you want the full comedy of that database bender, it’s in the advanced database guide, and the broader “stop optimizing before you have users” sermon is the broke solopreneur’s survival guide.)

Your users don’t care that your function has a stable reference. They care that the thing loads, works, and doesn’t cost you so much to run that you shut it down. Spend your energy on distribution, not on out-thinking a compiler that’s already smarter than both of us at this one specific job.

useCallback is a scalpel. It was never seasoning. And in 2026, the compiler is holding the scalpel — so put yours down and go ship something.

This is the Broken Engineer Guide. I over-engineer everything, fail at business, and share the scars so you can skip them. Go build something — and let the compiler sweat the re-renders.

Shubham Soni
Written by
Shubham Soni

A decade building, launching, and occasionally breaking SaaS products. I write SassTurf to share what actually moved the needle — free, no fluff.

Keep reading

Building

LangChain Alternatives That Actually Earn Their Place (2026)

9 min read
Building

Kubernetes Deployment: A Founder's Guide to the One File You'd Actually Write (2026)

9 min read
Email Marketing

Out of Office Email Templates That Don't Sound Like a Robot (2026)

8 min read

Enjoyed this? Get the next one.

One useful SaaS essay in your inbox each week. No fluff, unsubscribe anytime.