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