React Compiler: Automatic Memoization of Values
The problem with plain objects inside components
Every React render re-executes the component function. Any object literal you write runs again and produces a new reference — even when nothing about it has changed.
export default function Demo1() {
const [theme, setTheme] = useState('dark')
const [counter, setCounter] = useState(0)
const config = { theme }
return <Display config={config} />
}
When counter changes, the component re-renders. theme is still "dark", but config is a brand-new object:
Render 1: config → { theme: "dark" } ref A
Render 2: config → { theme: "dark" } ref B ← new reference
Same value. Different reference. React compares props by reference, so Display re-renders even though nothing it cares about has changed.
How we used to fix it: useMemo
Before the React Compiler, the standard fix was:
const config = useMemo(() => ({ theme }), [theme])
React stores the previous result and only recomputes when theme changes. If only counter changes, the cached object is returned — same reference, no child re-render.
This works, but it means every object, array, or derived value that gets passed as a prop or used in a dependency array needs to be wrapped. In a large codebase, useMemo and useCallback calls multiply fast and make the code harder to read.
How the React Compiler fixes it automatically
The React Compiler analyses your code at build time. When it sees:
const config = { theme }
It understands that config depends only on theme. Internally it transforms this into something equivalent to:
const config =
$cache[0] !== theme
? (($cache[0] = theme), ($cache[1] = { theme }))
: $cache[1]
If theme has not changed since the last render, the compiler returns the same cached object — identical reference. No useMemo required.
But it goes further. The compiler also sees:
<Display config={config} />
If config is the same reference, the compiler knows the child's props have not changed, so it skips rendering Display entirely. This is two levels of automatic caching: the value and the JSX.
Proving it with code
Here is the component from the demo app. There is no useMemo anywhere:
function Demo1_WithMemo() {
const [theme, setTheme] = useState('dark')
const [counter, setCounter] = useState(0)
// No useMemo — compiler caches this automatically
const config = { theme }
useRefCheck('Demo1.config', config)
return (
<div>
<Control
counter={counter}
theme={theme}
onForceRender={() => setCounter((c) => c + 1)}
onToggleTheme={() => setTheme((t) => (t === 'dark' ? 'light' : 'dark'))}
/>
<Display config={config} />
</div>
)
}
useRefCheck is a small helper that compares the current reference to the previous one and logs the result:
export function useRefCheck(label: string, value: unknown) {
const prev = useRef(value)
const same = prev.current === value
prev.current = value
addLog(label, same)
}
What happens when you click "Force re-render"
Only counter changes. theme stays "dark". The log shows:
[Demo1.config] ✅ same ref
The compiler returned the cached config. Display does not re-render.
What happens when you click "Toggle theme"
theme changes from "dark" to "light". Now the compiler creates a new object:
[Demo1.config] ❌ NEW ref
Display re-renders because it received a genuinely different config.
Disabling the compiler: "use no memo"
The React Compiler supports a per-component opt-out directive:
export default function Demo1_NoMemo() {
'use no memo'
const [theme, setTheme] = useState('dark')
const [counter, setCounter] = useState(0)
const config = { theme }
useRefCheck('Demo1.config', config)
return <Display config={config} />
}
"use no memo" is a compile-time directive — the compiler reads it during the build and skips all memoization transforms for this component. It cannot be toggled at runtime.
With the directive active, every render creates a new config — even when only counter changed:
Click "Force re-render":
[Demo1.config] ❌ NEW ref ← new object every time
Display re-renders on every click, and the flash indicator fires repeatedly. This is the behaviour we had before the compiler existed.
Comparing both versions in the same UI
Since "use no memo" is a build-time decision, the demo app uses two separate components and a toggle to switch between them:
export default function Demo1_AutoMemoValue() {
const [noMemo, setNoMemo] = useState(false)
return (
<div>
<button onClick={() => setNoMemo((v) => !v)}>
{noMemo ? '🚫 Compiler OFF' : '✅ Compiler ON'}
</button>
{noMemo ? <Demo1_NoMemo /> : <Demo1_WithMemo />}
</div>
)
}
Switching replaces one compiled component with another. State resets because these are genuinely different components — that is the point.
When memoization actually matters
Reference identity matters in three situations:
1. Preventing unnecessary child re-renders
If a child receives an object as a prop, a new reference means a new render — even if the data inside is identical.
2. Stabilising dependency arrays
useEffect(() => {
api.subscribe(config)
}, [config])
Without a stable reference, this effect runs on every render.
3. Avoiding expensive recomputation
const sorted = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items],
)
Heavy computation should not repeat when inputs have not changed.
When memoization does not matter
If the object is consumed locally and never passed as a prop or used in a dependency array:
const config = { theme }
console.log(config.theme)
Memoizing this provides zero benefit. The compiler is smart enough to skip memoization in cases where it would be useless — but even if it does cache it, the overhead is negligible.
What the compiler replaces
| Before the compiler | After the compiler |
|---|---|
useMemo(() => ({ theme }), [theme]) | const config = { theme } — compiler caches it |
useCallback((id) => delete(id), []) | plain function — compiler stabilises the reference |
React.memo(Child) | plain component — compiler skips re-render when props are unchanged |
You write straightforward code. The compiler handles reference stability.
The takeaway
The React Compiler automatically preserves reference identity for objects, arrays, and functions inside components. It analyses dependencies at build time, caches values when inputs have not changed, and skips child re-renders when props are stable.
useMemo and useCallback still work — the compiler respects them — but in most cases they are no longer necessary. You can write const config = { theme } and trust that the compiler will not create a new object unless theme actually changed.