You’ve been wrapping callbacks in useCallback, values in useMemo, and components in React.memo for years. The React Compiler shipped stable in React 19 and now runs in production at Meta. Every guide explains what the compiler IS. None of them tell you what to actually do differently Monday morning.
This one does. Three memoization habits you can delete, two cases where the compiler still can’t help you, and the eslint plugin you should run before you touch a single config file. The react 19 compiler migration is mostly subtraction. Let’s start with what survives.
Run the eslint plugin before you touch the compiler
eslint-plugin-react-compiler is the cheapest audit you’ll run this quarter. Install it, point it at your codebase, and it flags every place your code violates the Rules of React — mutation during render, side effects in render, conditional hook calls, unstable refs. These are the patterns the compiler silently bails out on. Worse, some of them are real bugs the compiler will turn from latent into visible.
npm install -D eslint-plugin-react-compiler
A real flag I caught last week: a component that pushed onto a ref during render to “track which items had been seen.” It worked fine because re-renders happened to fire in the right order. The compiler’s analysis flagged it as mutation during render, and rightly so — under concurrent rendering, that ref’s contents become non-deterministic.
The point: if half your components light up red, you’re not ready to enable the compiler. You have hidden bugs the compiler is about to expose. Fix the lint, then turn the switch. Reverse that order and you’ll spend a Tuesday debugging a “regression” that’s actually three years of latent bad code finally getting noticed.
Once the lint passes, you’re ready to start deleting. And the first hook you can stop maintaining by hand is the one we all reach for too often.
Habit it deletes #1: useMemo for render-time calculations
Before:
function Dashboard({ items, filter }) {
const visibleItems = useMemo(
() => items.filter(i => i.tag === filter).sort(byPriority),
[items, filter]
);
return <ItemList items={visibleItems} />;
}
After:
function Dashboard({ items, filter }) {
const visibleItems = items
.filter(i => i.tag === filter)
.sort(byPriority);
return <ItemList items={visibleItems} />;
}
The compiler reads which values the calculation depends on, emits memoization for you, and caches the result keyed on those exact inputs. You stop babysitting a dependency array that someone — probably future you — will forget to update after a refactor.
One honest caveat. The compiler memoizes inside a component or hook. It does not hoist the calculation to module scope. Render the same Dashboard ten times with the same props on one page, and you get ten memoized caches, not one shared result. For 99% of in-component derived data, that’s fine. For shared expensive work, we’ll get to that — it’s the first thing the compiler genuinely can’t fix.
One hook down. The next one is the one most teams overuse to the point of religion.
Habit it deletes #2: useCallback for stable child props
Before:
function TodoList({ todos, onToggle }) {
const handleToggle = useCallback(
id => onToggle(id),
[onToggle]
);
return todos.map(t =>
<TodoItem key={t.id} todo={t} onToggle={handleToggle} />
);
}
After:
function TodoList({ todos, onToggle }) {
return todos.map(t =>
<TodoItem
key={t.id}
todo={t}
onToggle={id => onToggle(id)}
/>
);
}
Yes, that’s an inline function in JSX. No, it doesn’t blow up your render performance. The compiler memoizes the JSX props object — the function identity stays stable across renders unless its captured values actually change. TodoItem only re-renders when its real inputs differ.
The caveat is narrow but important. The compiler stabilizes functions you pass to other React components. A callback you hand to a non-React subscriber — a WebSocket message handler, an IntersectionObserver, a third-party SDK — is not memoized for you. Wrap those by hand, or hoist them to module scope.
Two hooks gone. Time for the wrapper most teams treat as ritual.
Habit it deletes #3: React.memo wrappers everywhere
You know the codebase. Half the leaf components are exported as React.memo(Whatever) because somebody, somewhere, profiled a re-render and applied the wrapper as insurance. The wrappers stayed. They became culture.
Before:
const TodoItem = React.memo(function TodoItem({ todo, onToggle }) {
return (
<li onClick={() => onToggle(todo.id)}>
{todo.title}
</li>
);
});
After:
function TodoItem({ todo, onToggle }) {
return (
<li onClick={() => onToggle(todo.id)}>
{todo.title}
</li>
);
}
The compiler does the equivalent skip automatically. Children only re-render when their inputs are referentially different — and because the compiler now stabilizes those inputs upstream, the propagation just stops at the right boundary.
One exception that matters. If you used React.memo with a custom equality function — the second argument — you wrote it for a reason. Maybe you compare only the id of a complex object to dodge a deep-equal cost. Audit those before deleting. The compiler’s equality model is referential, and silently replacing your custom comparator can regress what you tuned by hand. The react performance patterns guide goes deeper on when custom equality earns its keep.
Three habits down. Now the harder question: where does the compiler stop?
Case it can’t handle #1: the same expensive calculation in multiple components
The compiler memoizes per component instance. Call expensivelyParse(url) inside three different components on the same page with the same URL, and it runs three times. Same story with useMemo — both have per-instance scope. This isn’t a bug; it’s the boundary the official docs spell out.
The fix lives outside React. Hoist the calculation to module scope and wrap it in a stable cache:
import memoize from 'memoize-one';
export const parseUrl = memoize(url => {
// expensive: regex, URL parsing, normalization
return doTheWork(url);
});
Now parseUrl('/foo') returns the same object reference whether it’s called from Header, Breadcrumbs, or useCurrentRoute. For longer-lived caches, swap memoize-one for an LRU. For truly global derived state, lift it into context or your state store — we covered the state management landscape if your team is still deciding which one.
Cross-component work needs cross-component memoization. The compiler won’t reach across React boundaries to do it for you. There’s one more place it pulls back — and the React team is explicit about this one.
Case it can’t handle #2: stabilizing useEffect dependencies
When a value flows into a useEffect dependency array, you need to control exactly when the effect fires. The compiler’s memoization decisions are correct for rendering, but they aren’t a contract you can rely on for effect timing.
function ResultsPanel({ filters }) {
const query = useMemo(
() => ({ tags: filters.tags, q: filters.search.trim() }),
[filters.tags, filters.search]
);
useEffect(() => {
fetchResults(query).then(setResults);
}, [query]);
}
Keep that useMemo. Delete it and your effect re-runs every render because query is a fresh object each time. The compiler’s memoization might stabilize it, but you don’t want “might” sitting between you and a network request.
useMemo here is an explicit contract: “this object’s reference only changes when its semantic value changes.” That’s a promise to the next reader of the file. Write it with a comment explaining why, so it doesn’t get deleted in a future cleanup pass that mistakes it for vestigial code. This is the official guidance from the React team, not a workaround.
You know what to delete and what to keep. Time to actually flip the switch.
Turn it on (and opt specific components out)
Vite:
npm install -D babel-plugin-react-compiler
// vite.config.js
import react from '@vitejs/plugin-react';
export default {
plugins: [
react({
babel: { plugins: ['babel-plugin-react-compiler'] }
})
]
};
Next.js 15.3.1+:
// next.config.js
module.exports = {
experimental: { reactCompiler: true }
};
The Next.js path is the cleaner one in 2026 — the swc-invoked compiler ships in the box, no Babel plugin required. First-class swc and oxc support is on the working group’s roadmap, so the Babel step will eventually go away for everyone.
When the compiler does something you didn’t expect — usually a stale value or a missing re-render — opt that file out with a directive at the top:
'use no memo';
function LegacyDashboard({ items }) {
// compiler skips this whole file
}
Use it sparingly. It’s a scalpel for files that violate the Rules of React and aren’t worth refactoring yet, or for a debugging session where you need to isolate compiler behavior. Don’t sprinkle it across the codebase. Don’t make it a habit.
Incremental adoption rule of thumb: start with leaf components, lint first, expand folder-by-folder.
The bottom line
Run eslint-plugin-react-compiler first. Delete useMemo for in-component calculations, useCallback for child handlers, and React.memo wrappers without custom equality. Keep useMemo for cross-component caching and for effect dependencies you need to control by hand. Flip experimental.reactCompiler in Next.js, or add the Babel plugin in Vite. Reach for 'use no memo' only when you have a specific bailout to isolate.
The React Compiler isn’t magic. It just automates the memoization patterns you were already supposed to be doing by hand — the ones we all forgot half the time and applied as ritual the other half. If your codebase follows the Rules of React, the react 19 compiler migration is mostly subtraction. And subtraction is the best kind of refactor.