React Compiler is not a new rendering model and it is not a replacement for good component design. It is an optimization pass that can automatically memoize eligible React code so you do less of that work by hand.
The useful part is not just that it exists. The useful part is how you install it, where it sits in the build pipeline, which options you actually need, and how you verify that it is doing useful work.
The short version
If your app is on React 19 and you are using a modern build setup, the default configuration is usually enough. Install the compiler plugin, place it early in the Babel pipeline, and verify the result in React DevTools. If your codebase is larger or more sensitive, adopt it in stages with directives, logging, and a narrow target version first.
React Compiler reduces some manual useMemo, useCallback, and React.memo usage, but it does not remove the need for TypeScript, clean data flow, or careful component boundaries.
Install the compiler
The core package is the Babel plugin. In most projects, you also want the React Hooks ESLint rules so the compiler can warn about code it cannot optimize.
npm install -D babel-plugin-react-compiler@latest eslint-plugin-react-hooks@latest
If you are targeting React 17 or React 18, the docs also call for the compiler runtime package and an explicit target setting. React 19 apps can usually start with the default behavior.
Start with the build tool you already have
React Compiler is easiest to adopt where you already control the Babel step. In a Vite app, that usually means adding the compiler inside @vitejs/plugin-react.
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: [
['babel-plugin-react-compiler', {
compilationMode: 'infer',
panicThreshold: 'none',
}],
],
},
}),
],
});
The important part is the order. React Compiler should run first so it can analyze the original source shape before other transforms rewrite it.
The configuration options that matter
Most apps do not need every option. In practice, only a few settings come up regularly.
| Option | What it controls | When to touch it |
|---|---|---|
| compilationMode | Whether the compiler infers what to optimize or only compiles annotated code | Use annotation for tight control, infer for normal adoption |
| target | Which React version the generated output should match | Set it when you are on React 17 or 18 |
| panicThreshold | Whether the build fails on unsupported code or skips problematic pieces | Use none if you want the compiler to skip issues instead of blocking the build |
| gating | Whether optimized code is controlled by a runtime feature flag | Use it for gradual rollouts and A/B testing |
| logger | How compilation events are reported | Use it when you want visibility into what compiled and what did not |
If you need a more explicit setup, a Babel config can make the intent easier to see.
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
target: '18',
compilationMode: 'annotation',
panicThreshold: 'none',
}],
],
};
That version is useful when you want the compiler to stay conservative while you validate a migration path.
How directives change usage
React Compiler directives give you function-level control. They are string literals that live at the top of a function body or the top of a module.
Use "use memo" when you want to opt a function into compilation in annotation mode, or when you want to make the compiler choice explicit. Use "use no memo" when you need to temporarily exclude a component that is not ready yet.
function SearchResults() {
"use memo";
return <div>Compiled search results</div>;
}
function ProblematicGrid() {
"use no memo";
return <div>Temporarily skipped</div>;
}
Directives are escape hatches, not the default operating mode. If you find yourself sprinkling them everywhere, the project probably needs a simpler rollout plan or a deeper review of the components that keep failing compilation.
What a real component looks like before and after
The useful mental model is not that the compiler rewrites your app into a different style. It is that it can preserve ordinary React code while removing some of the boilerplate around memoization.
import { useCallback, useMemo, useState } from 'react';
type Todo = {
id: number;
title: string;
completed: boolean;
};
type TodoListProps = {
todos: Todo[];
};
export function TodoList({ todos }: TodoListProps) {
const [filter, setFilter] = useState('');
const visibleTodos = useMemo(
() => todos.filter((todo) => todo.title.includes(filter)),
[todos, filter]
);
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setFilter(event.target.value);
}, []);
return (
<div>
<input value={filter} onChange={handleChange} />
<ul>
{visibleTodos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
}
import { useState } from 'react';
type Todo = {
id: number;
title: string;
completed: boolean;
};
type TodoListProps = {
todos: Todo[];
};
export function TodoList({ todos }: TodoListProps) {
const [filter, setFilter] = useState('');
const visibleTodos = todos.filter((todo) => todo.title.includes(filter));
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
setFilter(event.target.value);
}
return (
<div>
<input value={filter} onChange={handleChange} />
<ul>
{visibleTodos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
}
That is the real payoff: the code stays readable, but the compiler can remove some of the repetitive caching you used to maintain by hand.
Use ESLint as the early warning system
The React Hooks ESLint plugin is useful because it can point out patterns the compiler will skip. That is not a failure state; it is feedback you can work through incrementally.
If a component violates the Rules of React, the compiler should skip it rather than inventing unsafe output. The lint rule helps you find those cases before they become production surprises.
Verify that the compiler is active
The easiest check is React DevTools. Components optimized by React Compiler show a Memo ✨ badge in development mode. You can also inspect build output for compiler runtime references, which confirms that the transform is actually running.
If the badge does not appear, start with the basics: check plugin order, check the Babel config that Vite or your framework is actually loading, and make sure the code path is not excluded by a directive or a version mismatch.
Roll out gradually
The compiler is most useful when it is treated as a staged optimization, not a big-bang migration.
1 Start in infer mode
Let the compiler decide what it can optimize automatically, then review the results in DevTools and lint output.
2 Use annotation mode only when needed
Switch to annotation when you want explicit opt-in control through use memo.
3 Keep opt-outs temporary
Use use no memo as a short-term workaround while you fix the underlying component issue.
When not to use it
If your app is small, if render churn is not a problem, or if your build pipeline is already brittle, React Compiler may not pay for itself yet. It is an optimization layer, not a requirement for basic React development.
TypeScript still owns correctness. React Compiler only helps once the code is already valid and the remaining problem is repeated render work. That separation is the right mental model for deciding whether to adopt it.
Bottom line
React Compiler is easiest to use when you keep the setup boring: install the plugin, let it run early, default to the built-in behavior, and add configuration only when you have a concrete reason.
For most teams, the winning sequence is simple: start with the default setup, verify the compiler in DevTools, then use compilationMode, target, panicThreshold, or directives only when a specific rollout or compatibility need shows up.
The result is a React codebase that stays readable while the compiler handles some of the memoization work you would otherwise repeat by hand.