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:
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:
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: createOptionsEither include it or remove the dependency arrayI 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:
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:
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:
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:
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:
module.exports = { plugins: [ [ 'babel-plugin-react-compiler', { compilationMode: 'annotation' // Only compile explicitly marked components } ] ]};Then I opt in stable components:
function StableComponent() { "use memo"; // Opt into React Compiler // Component code}I keep problematic components manual:
function ProblematicComponent() { "use no memo"; // Fix issues before removing manual memoization // Component with manual optimization}Once I’m confident, I switch to automatic mode:
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:
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:
- 👨💻 React Compiler Documentation
- 👨💻 Reddit Discussion: Manual Memoization with React Compiler
- 👨💻 useEffect Dependencies Guide
- 👨💻 React Compiler Escape Hatches
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments