React Compiler: Automatic Memoization of Callbacks
The problem with inline functions inside components
Every React render re-executes the component function body. Any function you define inside is recreated as a new function object — even when its logic and captured variables are identical.
function Demo2() {
const [counter, setCounter] = useState(0)
const [clicks, setClicks] = useState(0)
const handleClick = () => {
setClicks((c) => c + 1)
}
return <ExpensiveChild onClick={handleClick} />
}
When counter changes, the component re-renders. handleClick is a brand-new function:
Render 1: handleClick → function() { ... } ref A
Render 2: handleClick → function() { ... } ref B ← new reference
Same logic. Different reference. React compares props by reference, so ExpensiveChild re-renders even though the callback hasn't meaningfully changed.
How we used to fix it: useCallback
Before the React Compiler, the standard fix was:
const handleClick = useCallback(() => {
setClicks((c) => c + 1)
}, [])
React stores the function and only creates a new one when the dependencies change. Since this callback has no dependencies (it uses the updater form of setClicks), the reference stays stable forever.
This works, but leads to the same boilerplate problem as useMemo — every handler, every event callback, every function passed as a prop needs wrapping. In a real component with five or six handlers, the noise adds up fast.
How the React Compiler fixes it automatically
The compiler analyses your code at build time. When it sees:
const handleClick = () => {
setClicks((c) => c + 1)
}
It understands that handleClick captures no changing variables — setClicks is a stable state setter from useState. So internally it transforms this into something equivalent to:
const handleClick =
$cache[0] ??
($cache[0] = () => {
setClicks((c) => c + 1)
})
The function is created once and reused on every subsequent render. No useCallback required.
But the compiler goes further. It also sees:
<ExpensiveChild onClick={handleClick} />
If handleClick is the same reference, the compiler knows the child's props haven't changed, so it skips rendering ExpensiveChild entirely. Two levels of caching: the function and the child JSX.
Proving it with code
Here is the component from the demo app. There is no useCallback anywhere:
function Demo2_WithMemo() {
const [counter, setCounter] = useState(0)
const [clicks, setClicks] = useState(0)
// No useCallback — compiler caches this automatically
const handleClick = () => {
setClicks((c) => c + 1)
console.log('Button clicked!')
}
useRefCheck('Demo2.handleClick', handleClick)
return (
<div>
<Control
counter={counter}
onForceRender={() => setCounter((c) => c + 1)}
/>
<ExpensiveChild onClick={handleClick} />
</div>
)
}
What happens when you click "Force re-render"
Only counter changes. handleClick has no dependency on counter. The log shows:
[Demo2.handleClick] ✅ same ref
The compiler returned the cached function. ExpensiveChild does not re-render.
What happens when you click the child's button
handleClick fires, which calls setClicks. The parent re-renders, but:
Button clicked!
[Demo2.handleClick] ✅ same ref
The function reference stays stable even after being called. The child is skipped again.
Disabling the compiler: "use no memo"
The "use no memo" directive tells the compiler to skip all memoization for a component:
export default function Demo2_NoMemo() {
'use no memo'
const [counter, setCounter] = useState(0)
const [clicks, setClicks] = useState(0)
const handleClick = () => {
setClicks((c) => c + 1)
console.log('Button clicked!')
}
useRefCheck('Demo2.handleClick', handleClick)
return <ExpensiveChild onClick={handleClick} />
}
With the directive active, every render creates a new handleClick — even when only counter changed:
Click "Force re-render":
[Demo2.handleClick] ❌ NEW ref ← new function every time
ExpensiveChild re-renders on every click. The flash indicator fires repeatedly, proving the child is no longer being skipped.
How this differs from value memoization
The previous article showed the compiler caching a plain object (const config = { theme }), replacing useMemo. This article shows it caching a function, replacing useCallback.
The mechanism is identical — the compiler analyses what the value depends on and reuses the cached version when inputs haven't changed. The difference is what gets cached:
| What | Before compiler | After compiler |
|---|---|---|
| Object | useMemo(() => ({ theme }), [theme]) | const config = { theme } |
| Function | useCallback(() => doSomething(), []) | const handleClick = () => doSomething() |
Both produce stable references without manual wrapping.
When callback stability actually matters
1. Preventing unnecessary child re-renders
The most common case. If a child receives a function as a prop:
<ExpensiveChild onClick={handleClick} />
A new function reference means a new render — even if the function does the exact same thing.
2. Stabilising effect dependencies
useEffect(() => {
window.addEventListener('click', handleClick)
return () => window.removeEventListener('click', handleClick)
}, [handleClick])
Without a stable reference, this effect tears down and re-attaches the listener on every render.
3. Context values
const value = { onSave: handleSave };
<FormContext.Provider value={value}>
If handleSave changes reference every render, every consumer of FormContext re-renders.
When it does not matter
If the function is called locally and never passed as a prop or used in a dependency array:
const format = (n) => n.toFixed(2)
return <span>{format(price)}</span>
Caching format provides zero benefit. The compiler may still cache it, but the overhead is negligible either way.
The takeaway
The React Compiler automatically preserves function reference identity inside components. It analyses captured variables at build time, caches the function when nothing it depends on has changed, and skips child re-renders when callback props are stable.
useCallback still works — the compiler respects it — but in most cases it is no longer necessary. You can write plain arrow functions and trust the compiler to keep them stable.