useMemo and useCallback confuse people because the thing they fix is invisible: object and function identity. So I built a tool that makes identity visible — it stamps a number on every reference so you can watch it stay stable or change, and counts when a React.memo child actually re-renders.
▶ Live demo: https://usememo-vs-usecallback.vercel.app/
Source (React 19 + TS): https://github.com/dev48v/usememo-vs-usecallback
The one fact everything hinges on
Every time a component renders, an inline object or function is a brand-new value:
// new {} and new () => {} on EVERY render
<Child config={{ mult }} onAction={() => doThing(mult)} />
React.memo compares props by reference (Object.is). So even though {mult: 2} looks identical to last render's {mult: 2}, it's a different object — and the memoized child re-renders anyway. One inline prop silently defeats memo.
The tool gives each reference a visible id. Click "re-render parent" with no memoization and you'll see config #5 → #6 → #7… while the child's render counter climbs in lockstep.
What useMemo / useCallback actually do
They cache the reference between renders:
const config = useMemo(() => ({ mult }), [mult]); // same object until mult changes
const onAction = useCallback(() => doThing(mult), [mult]); // same function until mult changes
Flip the toggles in the demo and the ids stop changing — so the memo child finally skips. Its render counter freezes no matter how many times the parent renders.
The subtlety people miss
Stable doesn't mean frozen. Bump the dependency (mult) and the memoized references do change — because the value they represent actually changed. That's correct: you want a new reference exactly when the data is new, and not a moment sooner. Watching the ids change only on a real dep change is what makes this click.
And the punchline: useCallback(fn, deps) is just useMemo(() => fn, deps). One memoizes a value, the other memoizes a function. Same machine.
Why a visualizer
You can read "memo does a shallow reference comparison" and still ship style={{...}} into a memoized component and wonder why it never skips. Seeing the reference id tick up on every render — then freeze the moment you wrap it — turns the rule into intuition.
Real React, zero UI dependencies. If it helped, a star helps others find it: https://github.com/dev48v/usememo-vs-usecallback
Top comments (2)
One thing that would make the visualizer even more convincing: a toggle that shows where useMemo does nothing for you. Wrap a primitive prop, or memoize for a child that isn't React.memo'd, and all that reference id bookkeeping is pure overhead with zero skipped renders. Half the people reaching for useMemo are wrapping things that were never the problem, and watching the render counter sit still while the cached value just costs memory would teach that as sharply as your current demo teaches the win. The "useCallback is just useMemo returning a function" line at the end is the cleanest way I've seen anyone put it, by the way.
This is super neat! I often find developers over-