React8 min read
React Compiler: Do You Still Need useMemo and useCallback?
React Compiler 1.0 memoizes components for you. Learn when useMemo and useCallback are still needed, how to enable it in Vite or Next.js 16, and what breaks.
Mostly, no. With React Compiler enabled (stable since v1.0 in October 2025), React memoizes components and hooks at build time, so you rarely need to write useMemo, useCallback or React.memo by hand in new code. You still reach for them as escape hatches, mainly when a value's identity matters to an effect, and you should not rip them out of existing code without testing. Here is how the compiler works, how to set it up, and where manual memoization still earns its place.
Key takeaways
- React Compiler is a build-time tool. It works with plain JavaScript and TypeScript and needs no code rewrites if you follow the Rules of React.
- For new code, the React team recommends relying on the compiler and using
useMemo/useCallbackonly where you need precise control. - For existing code, leave current memoization alone or test carefully before removing it.
- It supports React 17, 18 and 19. React 17/18 need
react-compiler-runtimeand atargetsetting. - In Next.js 16,
reactCompiler: trueis a stable top-level option. - Code that breaks the Rules of React is skipped, not "fixed". The ESLint plugin tells you where.
What React Compiler actually does
Without the compiler, React re-renders a component and all its children whenever its state changes. To avoid wasted work you had to do three things by hand:
- Wrap child components in
React.memo. - Wrap callbacks in
useCallbacksomemochildren see a stable prop. - Wrap expensive calculations and object props in
useMemo.
Get any one of those wrong, for example an inline object in the dependency array, and the whole chain stops working. I spent real time on this when tuning a service booking platform that used Redux and Zustand: code splitting, lazy loading and hand-written memoization, all reviewed component by component.
The compiler analyses each component and hook, works out which values depend on which inputs, and caches JSX and values automatically. It can even memoize values after an early return, which useMemo cannot do because hooks cannot be called conditionally.
// You write this
import { ProductRow, type Product } from './product-row';
type Props = { products: Product[]; query: string; onSelect: (id: string) => void };
export function ProductList({ products, query, onSelect }: Props) {
const visible = products.filter((p) =>
p.name.toLowerCase().includes(query.toLowerCase()),
);
return (
<ul>
{visible.map((p) => (
<ProductRow key={p.id} product={p} onClick={() => onSelect(p.id)} />
))}
</ul>
);
}With the compiler on, visible is only recomputed when products or query change, and the rendered JSX is reused when nothing relevant changed. You did not write a single memo hook.
How to enable React Compiler
The compiler ships as babel-plugin-react-compiler. The current npm release is 1.0.0.
Next.js 16
reactCompiler moved from experimental to a stable top-level option in Next.js 16.0.0. Next.js uses an SWC optimization so the Babel plugin only runs on relevant files (those with JSX or hooks).
npm install -D babel-plugin-react-compiler// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactCompiler: true,
};
export default nextConfig;To roll it out gradually, use annotation mode and opt components in with 'use memo':
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactCompiler: {
compilationMode: 'annotation',
},
};
export default nextConfig;export default function Dashboard() {
'use memo';
// ...
}Vite
For @vitejs/plugin-react 6.0.0 or later, the React Compiler installation docs show this setup:
npm install -D babel-plugin-react-compiler @rolldown/plugin-babel// vite.config.ts
import { defineConfig } from 'vite';
import react, { reactCompilerPreset } from '@vitejs/plugin-react';
import babel from '@rolldown/plugin-babel';
export default defineConfig({
plugins: [
react(),
babel({ presets: [reactCompilerPreset()] }),
],
});On older versions of the plugin, pass it through the babel option instead:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: { plugins: ['babel-plugin-react-compiler'] },
}),
],
});Plain Babel
The compiler must run first in your Babel plugin list:
// babel.config.js
module.exports = {
plugins: ['babel-plugin-react-compiler'],
};React 17 and 18
React 19 includes the runtime the compiler needs. On older versions, install the runtime as a normal dependency (not a devDependency) and set target:
npm install react-compiler-runtime@latest// babel.config.js
module.exports = {
plugins: [['babel-plugin-react-compiler', { target: '18' }]],
};When you still need useMemo and useCallback
The React docs keep both hooks as escape hatches. These are the cases where I still write them.
1. A value used as an effect dependency
The compiler decides what to memoize based on its own analysis. If an effect must only re-run when a value genuinely changes, make that explicit:
import { useEffect, useMemo } from 'react';
import { createConnection } from './chat';
export function ChatRoom({ roomId, serverUrl }: { roomId: string; serverUrl: string }) {
const options = useMemo(() => ({ roomId, serverUrl }), [roomId, serverUrl]);
useEffect(() => {
const conn = createConnection(options);
conn.connect();
return () => conn.disconnect();
}, [options]);
return <h1>Welcome to {roomId}</h1>;
}Even better: depend on the primitives (roomId, serverUrl) and build the object inside the effect. Then no memoization is needed at all.
2. Code the compiler skips
If a component breaks a rule (mutating props, reading a ref during render, calling a hook conditionally), the compiler bails out on that component and leaves it as plain React. Any manual memoization you had there keeps working. That is one reason the docs advise against removing existing useMemo calls without testing.
3. Existing code you have not tested
The compiler's output can differ depending on whether your manual memoization is there. Next.js exposes an enablePreserveExistingMemoizationGuarantees setting under reactCompiler.environment, and its docs recommend setting it explicitly because defaults may differ between compiler versions. Changing it, or deleting useMemo calls, is a behaviour change. Treat it like any other refactor.
| Situation | Without compiler | With compiler |
|---|---|---|
| Expensive derived value | useMemo |
Automatic |
| Callback passed to a child | useCallback + React.memo |
Automatic |
| Value that must be stable for an effect | useMemo |
useMemo (escape hatch) or depend on primitives |
| Component that breaks Rules of React | Manual memo works | Compiler skips it, manual memo still works |
| Existing memoized code | Keep | Keep, remove only with tests |
| Memoizing across components (global cache) | External store | External store (compiler only memoizes within a component or hook) |
What breaks under React Compiler
The compiler assumes your components follow the Rules of React: components are pure during render, props and state are treated as immutable, and hooks are called unconditionally. Code that "worked by accident" before can behave differently once memoized.
Mutating during render
// Problem: mutating the prop array
function SortedList({ items }: { items: string[] }) {
items.sort(); // mutates the parent's array
return <ul>{items.map((i) => <li key={i}>{i}</li>)}</ul>;
}
// Fix: copy first
function SortedListFixed({ items }: { items: string[] }) {
const sorted = items.toSorted();
return <ul>{sorted.map((i) => <li key={i}>{i}</li>)}</ul>;
}Reading refs during render
Reading ref.current in the render body gives a value React cannot track. Read refs in effects or event handlers.
Opting a component out
When a component misbehaves after compilation, add 'use no memo' while you fix the root cause:
function LegacyChart() {
'use no memo';
// ...
}The docs describe this as a temporary measure, not a permanent setting.
Use the ESLint plugin before you flip the switch
The compiler's diagnostics now live in eslint-plugin-react-hooks (7.x). It includes compiler-powered rules such as react-hooks/purity, react-hooks/refs, react-hooks/immutability, react-hooks/set-state-in-render and react-hooks/set-state-in-effect, and the React team ships them in the recommended preset.
npm install -D eslint-plugin-react-hooks@latest// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';
export default defineConfig([
reactHooks.configs.flat.recommended,
]);When a rule reports an error, the compiler skips that component or hook. You do not have to fix every violation before enabling the compiler, but the list tells you exactly which parts of your app are not getting optimized. The set-state-in-effect rule in particular catches a lot of the patterns I describe in Common useEffect mistakes.
How to verify it is working
- Run the app in development.
- Open React DevTools.
- Look for the "Memo ✨" badge next to component names.
You can also check the build output for an import from react/compiler-runtime.
What gains should you expect? The only production numbers I trust are the React team's own: in the React Compiler v1.0 announcement they report initial loads and cross-page navigations on the Meta Quest Store improving by up to 12%, with some interactions more than 2.5x faster, and they add that your mileage may vary. For your own app, measure with the React DevTools Profiler or the Performance panel before and after. The compiler cuts re-renders, but if your slowness is a 2 MB bundle or a waterfall of requests, it will not help. My React performance optimization checklist covers those other causes.
A practical migration plan
- Upgrade
eslint-plugin-react-hooksand fix the errors that are cheap to fix. - Enable the compiler in annotation mode, or for one route or package first.
- Run your test suite and click through key flows. Look for stale UI or effects that fire too often or too rarely.
- Switch to full compilation.
- Stop adding new
useMemo/useCallbackby default. Remove old ones only when you touch that code and have tests around it.
FAQ
Does React Compiler replace useMemo and useCallback?
For most new code, yes. The compiler applies memoization automatically. The React docs still recommend useMemo and useCallback as escape hatches when you need precise control, such as a value used as an effect dependency.
Should I delete existing useMemo and useCallback calls after enabling the compiler?
Not in bulk. The React docs recommend leaving existing memoization in place or testing carefully before removing it, because removing it can change the compiled output.
Does React Compiler work with React 18?
Yes. The compiler supports React 17, 18 and 19. For 17 and 18 you set the target option and install react-compiler-runtime as a regular dependency.
How do I know if a component was compiled?
Open React DevTools in development. Components optimized by the compiler show a Memo badge with a sparkle next to their name.
How do I turn off the compiler for one component?
Add the 'use no memo' directive at the top of the component or hook body. Treat it as a temporary fix while you resolve the underlying issue.
Wrapping up
React Compiler moves memoization from a code review checklist to the build step. Turn it on, let the ESLint rules show you which components it cannot optimize, and keep useMemo and useCallback for the few places where identity truly matters. Your new components get shorter, and the memoization bugs mostly go away.
- #react
- #react-compiler
- #performance
- #hooks
Keep reading
React7 min read
10 Custom React Hooks Worth Having (TypeScript Examples)
10 custom React hooks in TypeScript: useDebounce, useLocalStorage, useMediaQuery, useOnClickOutside, useInterval and more, built for React 19 and the compiler.
Read articleReact8 min read
React 19 Hooks: use, useActionState, useOptimistic Explained
React 19 hooks explained with TypeScript examples: use, useActionState, useOptimistic and useFormStatus, plus the gotchas that trip people up in real forms.
Read articleReact9 min read
React Performance Optimization Checklist (2026 Edition)
A practical React performance optimization checklist: profile first, cut re-renders, memoize wisely, virtualize long lists, code split and shrink your bundle.
Read article