Skip to content

When Should You Still Use Manual Memoization with React Compiler?

Purpose

When I started using React Compiler, I wondered if I could remove all my useMemo, useCallback, and React.memo calls. The compiler promises automatic optimization, but I found that’s not the full story.

This post shows when you still need manual memoization with React Compiler. The key point is knowing the escape hatch scenarios where automatic optimization falls short.

Environment

  • React 19 with React Compiler
  • React DevTools Profiler
  • TypeScript 5.7

What I tried

I enabled React Compiler on a large codebase and removed all manual memoization:

Before React Compiler
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]);
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]);
}

I removed the memoization:

After removing memoization
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = () => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
};
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // Problem: createOptions changes on every render
}

Then I got this issue:

Warning: Effect has missing dependency: createOptions
Either include it or remove the dependency array

I thought React Compiler would handle this automatically. But the effect was firing on every render, causing my chat connection to disconnect and reconnect repeatedly.

What I discovered

I checked the React Compiler documentation and found that manual memoization is an “escape hatch”. The compiler preserves existing useMemo and useCallback calls rather than removing them.

I found four scenarios where manual memoization is still necessary:

Effect Dependencies

When a memoized value is used as a dependency in useEffect, manual memoization prevents the effect from firing repeatedly:

Using useCallback for effect stability
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // Only changes when roomId changes
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // Effect only runs when roomId changes
}

React Compiler automatically infers dependencies in most cases, but I found it doesn’t always understand when I want precise control over when effects fire.

Context Value Stability

Without memoization, the entire context tree re-renders when any parent updates:

Stabilizing Context values
function MyApp({ currentUser }) {
const login = useCallback((response) => {
storeCredentials(response.credentials);
setCurrentUser(response.user);
}, []);
const contextValue = useMemo(() => ({
currentUser,
login
}), [currentUser, login]);
return (
<AuthContext value={contextValue}>
<Page />
</AuthContext>
);
}

I tried removing this memoization, and the profiler showed every consumer re-rendering even when currentUser didn’t change.

Third-Party Library Integrations

I use animated libraries and data grids that require stable function references. React Compiler can’t infer these requirements because they’re external to React:

Stable references for libraries
function DataGrid({ data }) {
const handleRowClick = useCallback((rowId) => {
navigate(`/rows/${rowId}`);
}, [navigate]); // Stable reference for grid component
return (
<Grid
data={data}
onRowClick={handleRowClick} // Grid expects stable callback
/>
);
}

I removed useCallback here, and the grid lost its scroll position and selection state on every render.

Performance Debugging

When I need to isolate specific re-render patterns, manual memoization gives me granular control:

Debugging performance issues
function Component({ data, filter }) {
const filtered = useMemo(
() => data.filter(filter),
[data, filter] // I can verify these are the only dependencies
);
return <List items={filtered} />;
}

I can use React DevTools to see exactly when filtered recalculates. With automatic optimization, it’s harder to trace what changed.

Gradual Adoption Strategy

I learned not to remove all manual memoization at once. React Compiler supports annotation mode for controlled rollout:

babel.config.js
module.exports = {
plugins: [
[
'babel-plugin-react-compiler',
{
compilationMode: 'annotation' // Only compile explicitly marked components
}
]
]
};

Then I opt in stable components:

Opting into compilation
function StableComponent() {
"use memo"; // Opt into React Compiler
// Component code
}

I keep problematic components manual:

Keeping manual optimization
function ProblematicComponent() {
"use no memo"; // Fix issues before removing manual memoization
// Component with manual optimization
}

Once I’m confident, I switch to automatic mode:

Automatic compilation
module.exports = {
plugins: [
[
'babel-plugin-react-compiler',
{
compilationMode: 'infer' // Compile everything except "use no memo"
}
]
]
};

Common Mistakes

I made these errors when adopting React Compiler:

Removing all memoization without testing

I deleted useMemo and useCallback everywhere. Some components started re-rendering excessively. The compiler preserves existing memoization, so I should have left it in place initially.

Incomplete dependency arrays

I wrote code like this:

Wrong: missing dependency
const filtered = useMemo(
() => data.filter(filter),
[data] // Missing 'filter' dependency
);

React Compiler cannot optimize code with incomplete dependencies. The rule: either include all dependencies or let the compiler handle it automatically.

Using manual memoization everywhere

I added useMemo to every calculation. This defeated the purpose of automatic optimization and made the code harder to maintain. Now I only use it for the four escape hatch scenarios I mentioned.

Summary

In this post, I showed when manual memoization is still necessary with React Compiler. The key point is understanding that automatic optimization is powerful but not omnipotent.

Use manual memoization as an escape hatch for:

  • Effect dependencies to prevent infinite loops
  • Context values to avoid unnecessary re-renders
  • Third-party library integrations requiring stable references
  • Performance debugging when you need granular control

Don’t remove all useMemo and useCallback calls immediately. React Compiler preserves existing memoization, so keep it in place until you identify specific problems. For new code, rely on the compiler first, then add manual memoization only when needed.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments