Every React developer has written code like useMemo(() => ..., [a, b]) or useCallback(fn, []) and wondered, “Do I really need this here?” The React Compiler answers that question for you. It is a build-time tool that reads your components and adds memoization automatically, so your app skips unnecessary re-renders without you sprinkling useMemo, useCallback and React.memo everywhere.
In this React Compiler tutorial, you will learn what the compiler does, how to set it up in Vite, Next.js and plain Babel projects, how to check that it is working, which coding rules it expects, and how to roll it out safely in an existing project.
- React Compiler is a build-time tool (a Babel plugin) that automatically memoizes components, values and functions.
- Version 1.0 is stable and is used in production at Meta. It works best with React 19, and also supports React 17 and 18.
- Setup is one package (
babel-plugin-react-compiler) plus one config change. In Next.js 16 it is justreactCompiler: true. - Your code must follow the Rules of React (pure render, no mutating props or state). The ESLint plugin
eslint-plugin-react-hookswarns you when it doesn’t. - For new code, skip
useMemoanduseCallback. For old code, leave existing memoization in place unless you test carefully.
What Is the React Compiler?
The React Compiler is an optimizing compiler built by the React team at Meta. It runs while your app is being built (not in the browser), looks at each component and custom hook, and rewrites it so that values and JSX are only recalculated when their inputs actually change.
The React team released React Compiler v1.0 as stable in October 2025. Meta had already been running it in production — on the Meta Quest Store they reported up to 12% faster initial loads and page navigations, and some interactions becoming more than 2.5× faster, with no increase in memory use.
Why Manual Memoization Is Painful
In React, when a parent component re-renders, all its children re-render too by default. Usually that is fine. But in large lists, dashboards or forms it can make the UI feel slow. Until now, the fix was manual memoization:
React.memo— skip re-rendering a child if its props did not change.useMemo— remember the result of an expensive calculation.useCallback— keep the same function reference between renders, so a memoized child doesn’t re-render.
The problem is that these tools are easy to get wrong. Forget one item in a dependency array and you get stale data. Pass a new inline object to a memoized child and all your React.memo work is wasted. Add them everywhere “just in case” and your code becomes hard to read. If you have struggled with dependency arrays before, our guide on the useEffect hook explains why they matter so much.
App changes, only the parts that use it need to re-render. The compiler caches the rest for you.How the React Compiler Works
The compiler reads your component like a very careful code reviewer. It figures out which values depend on which props and state, and then wraps each piece in a small cache. On the next render, if the inputs are the same, React reuses the cached value instead of recalculating it.
Here is a tiny component and a simplified version of what the compiler turns it into:
export function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
import { c as _c } from "react/compiler-runtime";
export function Greeting(t0) {
const $ = _c(2); // a cache with 2 slots for this component
const { name } = t0;
let t1;
if ($[0] !== name) { // did "name" change since last render?
t1 = <h1>Hello, {name}!</h1>;
$[0] = name;
$[1] = t1;
} else {
t1 = $[1]; // reuse the cached JSX
}
return t1;
}
You never write or read this output yourself — it is just useful to know what is going on. Because the cached JSX is the same object as last time, React knows it can skip that part of the tree.
return. You cannot do that with useMemo, because hooks are not allowed after a conditional return.function Report({ user, rows }) {
if (!user) return <p>Please sign in.</p>;
// The compiler caches this calculation even though it is
// after an early return. A useMemo here would break the Rules of Hooks.
const total = rows.reduce((sum, row) => sum + row.amount, 0);
return <Summary total={total} rows={rows} />;
}
How to Set Up React Compiler
The setup depends on your tool. Pick your tab below. All three use the same package, babel-plugin-react-compiler, from the official installation guide.
Vite with @vitejs/plugin-react 6 or newer (Vite 8). Version 6 of the React plugin no longer runs Babel itself, so you add the Rolldown Babel plugin and use the compiler preset:
npm install -D babel-plugin-react-compiler @rolldown/plugin-babel
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()],
}),
],
});
Older @vitejs/plugin-react (5.x and below): pass the plugin through the built-in babel option instead.
// vite.config.js (plugin-react 5.x or older)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: ['babel-plugin-react-compiler'],
},
}),
],
});
Next.js 16 and newer. The reactCompiler option is stable and lives at the top level of your config. Next.js also uses a fast SWC check so the compiler only runs on files that contain JSX or hooks.
npm install -D babel-plugin-react-compiler
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactCompiler: true,
};
export default nextConfig;
On Next.js 15 the same option lived under experimental.reactCompiler. If you are also using caching in the App Router, see our guide to Next.js use cache — the two features work well together.
Any Babel-based setup (custom webpack, Metro for React Native, etc.). The compiler must run first in the plugin list:
npm install -D babel-plugin-react-compiler
module.exports = {
plugins: [
'babel-plugin-react-compiler', // must run first!
// ...your other plugins
],
};
Expo: new apps created with npx create-expo-app@latest (SDK 54 and up) already have the compiler turned on.
npm create vite@latest and npx create-next-app@latest both let you pick a template with the compiler already enabled.Using React 17 or 18
The compiler is designed for React 19, where the cache helper lives inside react/compiler-runtime. On React 17 or 18, install the small runtime package as a normal dependency (not a dev dependency) and tell the compiler which version you use:
npm install react-compiler-runtime@latest
const ReactCompilerConfig = {
target: '18', // use '17' for React 17 — a string, major version only
};
module.exports = {
plugins: [
['babel-plugin-react-compiler', ReactCompilerConfig],
],
};
Before and After: A Real Example
Let’s look at a typical product list with a search box and an “Add to cart” button. This is how many of us wrote it before the compiler:
import { memo, useCallback, useMemo, useState } from 'react';
const ProductRow = memo(function ProductRow({ product, onAdd }) {
return (
<li>
{product.name} — ₹{product.price}
<button onClick={() => onAdd(product.id)}>Add</button>
</li>
);
});
export default function ProductList({ products }) {
const [query, setQuery] = useState('');
const [cart, setCart] = useState([]);
const visible = useMemo(
() =>
products.filter((p) =>
p.name.toLowerCase().includes(query.toLowerCase())
),
[products, query]
);
const handleAdd = useCallback((id) => {
setCart((current) => [...current, id]);
}, []);
return (
<section>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products"
/>
<p>Items in cart: {cart.length}</p>
<ul>
{visible.map((p) => (
<ProductRow key={p.id} product={p} onAdd={handleAdd} />
))}
</ul>
</section>
);
}
With the React Compiler turned on, you can write the same thing as plain, readable React:
import { useState } from 'react';
function ProductRow({ product, onAdd }) {
return (
<li>
{product.name} — ₹{product.price}
<button onClick={() => onAdd(product.id)}>Add</button>
</li>
);
}
export default function ProductList({ products }) {
const [query, setQuery] = useState('');
const [cart, setCart] = useState([]);
const visible = products.filter((p) =>
p.name.toLowerCase().includes(query.toLowerCase())
);
function handleAdd(id) {
setCart((current) => [...current, id]);
}
return (
<section>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products"
/>
<p>Items in cart: {cart.length}</p>
<ul>
{visible.map((p) => (
<ProductRow key={p.id} product={p} onAdd={handleAdd} />
))}
</ul>
</section>
);
}
The compiler caches visible (recalculated only when products or query change), keeps handleAdd stable, and memoizes each ProductRow‘s JSX. When you click “Add”, the cart count updates but the product rows are reused from the cache.
Try it yourself
Add a quick log to see the difference in your own project:
function ProductRow({ product, onAdd }) {
console.log('render row', product.id); // remove after testing
// ...
}
Without the compiler, every click on “Add” logs every row. With the compiler, typing in the search box only logs rows whose data actually changed, and clicking “Add” logs none.
How to Check It Is Working
- Install the React Developer Tools browser extension.
- Run your app in development mode (
npm run dev). - Open the Components tab. Components optimized by the compiler show a “Memo ✨” badge next to their name.
You can also look at the built code. Compiled components import c from react/compiler-runtime (or from react-compiler-runtime on React 17/18) and use a $ cache array, like in the example above.
If a component has no badge, the compiler decided to skip it — usually because the code breaks one of the rules in the next section. The ESLint plugin will tell you why.
Rules Your Code Must Follow
The compiler assumes your components follow the Rules of React. In simple words: rendering must be a pure calculation. Given the same props and state, a component should return the same JSX and should not change anything outside itself. Here are the four mistakes that most often stop the compiler.
1. Mutating props or state
// ❌ Bad: .sort() changes the original array that came from props
function TopScores({ scores }) {
const sorted = scores.sort((a, b) => b - a);
return <List items={sorted} />;
}
// ✅ Good: make a sorted copy instead
function TopScores({ scores }) {
const sorted = scores.toSorted((a, b) => b - a); // or [...scores].sort(...)
return <List items={sorted} />;
}
2. Reading or writing refs during render
// ❌ Bad: ref.current is read while rendering
function Timer() {
const startRef = useRef(Date.now());
return <p>Started at {startRef.current}</p>;
}
// ✅ Good: store values you show on screen in state
function Timer() {
const [startedAt] = useState(() => Date.now());
return <p>Started at {startedAt}</p>;
}
Reading ref.current inside event handlers and effects is completely fine — just not in the render body.
3. Impure values during render
// ❌ Bad: a new random id on every render
function Badge() {
const id = Math.random();
return <span id={id}>New</span>;
}
// ✅ Good: use the built-in hook for stable ids
import { useId } from 'react';
function Badge() {
const id = useId();
return <span id={id}>New</span>;
}
4. Calling setState during render
// ❌ Bad: setting state while rendering causes extra renders or loops
function Cart({ items }) {
const [count, setCount] = useState(0);
setCount(items.length);
return <p>{count} items</p>;
}
// ✅ Good: derive the value directly — no state needed
function Cart({ items }) {
const count = items.length;
return <p>{count} items</p>;
}
Catch Problems Early with ESLint
The compiler’s checks are built into the latest eslint-plugin-react-hooks. Its recommended preset now includes rules such as purity, refs, immutability, set-state-in-render, set-state-in-effect and incompatible-library, alongside the classic rules-of-hooks and exhaustive-deps.
npm install -D eslint-plugin-react-hooks@latest
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';
export default defineConfig([
reactHooks.configs.flat.recommended,
]);
Tip: turn on the lint rules before you turn on the compiler. Fixing the warnings first makes the switch much smoother — and the rules are useful even if you never enable the compiler.
Adopting It in an Existing App
For a brand-new project, just enable the compiler for everything. For a big existing codebase, go step by step.
Option A: opt-in mode with “use memo”
Set compilationMode to 'annotation'. Now the compiler only touches components and hooks that start with the "use memo" directive.
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactCompiler: {
compilationMode: 'annotation',
},
};
export default nextConfig;
export default function Dashboard() {
'use memo'; // only this component is compiled
// ...
}
With Babel, pass the same option: ['babel-plugin-react-compiler', { compilationMode: 'annotation' }].
Option B: compile everything, opt out problem files
If one component misbehaves after enabling the compiler, add "use no memo" at the top of it. Treat this as a temporary bandage while you fix the underlying issue.
function LegacyChart({ data }) {
'use no memo'; // skip this component for now
// ...
}
Should you delete your existing useMemo and useCallback?
The React team’s advice is clear:
- New code: rely on the compiler. Use
useMemo/useCallbackonly when you need exact control — for example, a value used as an effect dependency that must not change. - Existing code: leave current memoization in place, or remove it only with careful testing. Removing it can change what the compiler outputs.
There is no rush. Keeping old hooks around does no harm; the compiler works with them.
Common Errors and Fixes
| Problem | Likely cause | Fix |
|---|---|---|
| No “Memo ✨” badge in DevTools | Compiler not running, or it skipped the component | Check your config; run ESLint to see which rule the component breaks |
Vite: the babel option in react() does nothing | @vitejs/plugin-react 6 removed the inline Babel option | Use @rolldown/plugin-babel with reactCompilerPreset() |
Cannot find module 'react/compiler-runtime' | Project uses React 17 or 18 | Install react-compiler-runtime and set target: '18' (or '17') |
Next.js 16 ignores or warns about experimental.reactCompiler | In Next.js 16 the option moved out of experimental | Use top-level reactCompiler: true |
| UI does not update after you change an array or object | Code mutates state or props directly | Create new arrays/objects ([...arr], {...obj}, toSorted()) |
| A third-party hook returns stale data | Library returns mutable objects the compiler can’t track | Watch for the incompatible-library lint warning; add "use no memo" to that component |
| Effect runs more or less often than before | Effect depended on values that are now memoized differently | Keep an explicit useMemo/useCallback for effect dependencies that must stay stable |
Best Practices
- Lint first, compile second. Turn on
eslint-plugin-react-hooksand fix warnings before switching the compiler on. - Measure, don’t guess. Use the React DevTools Profiler before and after enabling the compiler so you can see the real gain.
- Keep render pure. No API calls, no random values, no mutations in the component body. Put side effects in event handlers or effects.
- Prefer derived values over extra state. If something can be calculated from props or state, calculate it — the compiler will cache it.
- Roll out gradually in big apps. Start with annotation mode or a single folder, ship it, watch for bugs, then expand.
- Use “use no memo” as a short-term fix only. Leave a TODO and come back to fix the real problem.
- Have good tests. Memoization changes when code runs. End-to-end and component tests give you confidence during the switch.
- Typing matters too. If you are upgrading tooling at the same time, our TypeScript 7 migration guide covers the compiler side of TypeScript.
FAQ
Is React Compiler ready for production?
Yes. Version 1.0 was released as stable in October 2025, and Meta uses it in production apps such as the Meta Quest Store.
Do I still need useMemo and useCallback?
Mostly no for new code. Keep them for special cases where you need exact control, like a value used as a dependency of useEffect. Existing ones can stay in place.
Does React Compiler work with React 18?
Yes. Set target: '18' in the compiler options and install react-compiler-runtime as a regular dependency. React 17 is supported the same way with target: '17'.
Will it make my build slower?
A little, because Babel has to run on your components. In Next.js the impact is small because it only compiles files that contain JSX or hooks.
Does it work with TypeScript?
Yes. The compiler works on .tsx and .ts files the same way it works on JavaScript.
What happens if my code breaks the Rules of React?
The compiler skips optimizing that component or hook and compiles the rest. Your app still works; that component just doesn’t get the speed boost.
Can I use it with React Native?
Yes. It works through Babel, and new Expo apps (SDK 54 and up) have it enabled by default.
Conclusion
The React Compiler is one of the biggest quality-of-life upgrades React has had in years. Instead of guessing where to add useMemo, useCallback and React.memo, you write simple, readable components and let the build step handle performance. Turn on the ESLint rules, enable the compiler with one config change, check for the “Memo ✨” badge, and roll it out step by step in older apps.
Try it today on one page of your project — you will likely delete more code than you add.
New posts every day on DevDojo
Practical tutorials on React, Next.js, Node.js, Python, AI and DevOps — written in simple English for developers in India and around the world. Bookmark devdojo.co.in and drop your questions in the comments below!