JavaScript and React course Β· Module 10: React Ecosystem and Future

React Compiler (React Forget)

8 min read
In this lesson8

Imagine that on your spaceship, instead of manually configuring every optimization system, you have an intelligent autopilot that automatically detects and optimizes all redundant operations. That is exactly what the React Compiler (also known as React Forget) does - it automatically analyzes your code and adds memoization where it is needed. Version 1.0 has been stable since October 2025 and works with React 17, 18 and 19, best with 19.

The Problem: Manual Memoization

Until now, performance optimization in React required manual use of React.memo, useMemo, and useCallback. It is like manually configuring every system on a spaceship - time-consuming, error-prone, and easy to overlook:

1// BEFORE React Compiler - manual memoization
2import { useState, useMemo, useCallback, memo } from 'react';
3
4const MissionCard = memo(function MissionCard({ mission, onSelect }) {
5  return (
6    <div onClick={() => onSelect(mission.id)}>
7      <h3>{mission.name}</h3>
8      <p>Status: {mission.status}</p>
9    </div>
10  );
11});
12
13function MissionDashboard({ missions }) {
14  const [filter, setFilter] = useState('all');
15  const [selectedId, setSelectedId] = useState(null);
16
17  // You must remember to use useMemo...
18  const filteredMissions = useMemo(
19    () => missions.filter(m => filter === 'all' || m.status === filter),
20    [missions, filter]
21  );
22
23  // ...and useCallback for every function passed as a prop
24  const handleSelect = useCallback((id) => {
25    setSelectedId(id);
26  }, []);
27
28  return (
29    <div>
30      {filteredMissions.map(m => (
31        <MissionCard key={m.id} mission={m} onSelect={handleSelect} />
32      ))}
33    </div>
34  );
35}

Problems with manual memoization:

  • Easy to forget - miss a useCallback and React.memo won't work
  • Boilerplate - lots of code unrelated to business logic
  • Wrong dependencies - incorrect dependency array in useMemo/useCallback = bug
  • Over-memoization - memoizing things that don't need optimization

React Compiler - Automatic Memoization

React Compiler is a build-time tool that automatically analyzes React components and adds appropriate memoization. It works at compilation time - your source code remains clean:

1// AFTER React Compiler - write plain, clean code
2function MissionDashboard({ missions }) {
3  const [filter, setFilter] = useState('all');
4  const [selectedId, setSelectedId] = useState(null);
5
6  // Compiler AUTOMATICALLY memoizes this computation
7  const filteredMissions = missions.filter(
8    m => filter === 'all' || m.status === filter
9  );
10
11  // Compiler AUTOMATICALLY stabilizes this function
12  const handleSelect = (id) => {
13    setSelectedId(id);
14  };
15
16  return (
17    <div>
18      {filteredMissions.map(m => (
19        <MissionCard key={m.id} mission={m} onSelect={handleSelect} />
20      ))}
21    </div>
22  );
23}
24
25// You don't need React.memo - Compiler handles it!
26function MissionCard({ mission, onSelect }) {
27  return (
28    <div onClick={() => onSelect(mission.id)}>
29      <h3>{mission.name}</h3>
30      <p>Status: {mission.status}</p>
31    </div>
32  );
33}

How Does React Compiler Work?

The Compiler analyzes your code during the build (e.g., with a Babel plugin) and generates an optimized version:

1// Your source code (what you write):
2function StarList({ stars, highlight }) {
3  const visible = stars.filter(s => s.magnitude < 6);
4  const style = { color: highlight ? '#ffd700' : '#ccc' };
5
6  return (
7    <ul style={style}>
8      {visible.map(s => <li key={s.id}>{s.name}</li>)}
9    </ul>
10  );
11}
12
13// What the Compiler generates (simplified example):
14function StarList_compiled({ stars, highlight }) {
15  // Compiler adds cache for filtering
16  const visible = useMemo(
17    () => stars.filter(s => s.magnitude < 6),
18    [stars]
19  );
20
21  // Compiler caches the style object
22  const style = useMemo(
23    () => ({ color: highlight ? '#ffd700' : '#ccc' }),
24    [highlight]
25  );
26
27  return (
28    <ul style={style}>
29      {visible.map(s => <li key={s.id}>{s.name}</li>)}
30    </ul>
31  );
32}

The Compiler does this intelligently - it does not memoize everything, only those parts that can actually repeat with the same data.

Configuring React Compiler

Installation with Babel

The compiler is the Babel plugin babel-plugin-react-compiler, which you install as a dev dependency. In the config it must come first in the plugin list, because it needs the original code before other transforms change it:

1npm install -D babel-plugin-react-compiler@latest
1// babel.config.js
2module.exports = {
3  plugins: [
4    // React Compiler must run first
5    ['babel-plugin-react-compiler', {
6      compilationMode: 'infer', // default mode
7      panicThreshold: 'none', // a component with an error stays unoptimized, and the build goes on
8    }],
9    // ...other plugins
10  ],
11};

In a React 17 or 18 project, add the target: '18' (or '17') option and install the react-compiler-runtime package, because the compiled code then needs that library. In Vite 8 with @vitejs/plugin-react 6 you add the compiler through @rolldown/plugin-babel and the reactCompilerPreset preset:

1// vite.config.js
2import { defineConfig } from 'vite';
3import react, { reactCompilerPreset } from '@vitejs/plugin-react';
4import babel from '@rolldown/plugin-babel';
5
6export default defineConfig({
7  plugins: [
8    react(),
9    babel({
10      presets: [reactCompilerPreset()]
11    }),
12  ],
13});

Installation with Next.js

In Next.js 16 you install the same plugin as a dev dependency and turn on the reactCompiler option at the top level of the config. In earlier Next.js versions it lived in the experimental section. Next.js runs the plugin only for files with JSX or hooks, so the build slows down only slightly:

1// next.config.js
2const nextConfig = {
3  reactCompiler: true,
4};
5
6module.exports = nextConfig;

You can also turn the compiler on gradually: reactCompiler: { compilationMode: 'annotation' } compiles only components with the 'use memo' directive at the top of the function, and the 'use no memo' directive turns compilation off for a chosen component.

Compilation Modes

  • infer (default) - compiles functions named like components (PascalCase) or hooks (use...) that create JSX or call other hooks, plus functions with the 'use memo' directive
  • annotation - compiles only functions marked with the 'use memo' directive, which makes gradual adoption easier
  • all - compiles all top-level functions, including ones that are not components, which is why it is not recommended

Rules of React

React Compiler works correctly ONLY if your code follows the "Rules of React":

1// GOOD - pure function, no side effects in render
2function GoodComponent({ data }) {
3  const sorted = [...data].sort((a, b) => a.name.localeCompare(b.name));
4  return <ul>{sorted.map(d => <li key={d.id}>{d.name}</li>)}</ul>;
5}
6
7// BAD - mutating props (Compiler may generate incorrect code!)
8function BadComponent({ data }) {
9  data.sort((a, b) => a.name.localeCompare(b.name)); // Mutation!
10  return <ul>{data.map(d => <li key={d.id}>{d.name}</li>)}</ul>;
11}
12
13// BAD - side effect in render
14function BadComponent2({ userId }) {
15  // Compiler will assume this is pure and may skip it!
16  analytics.track('render', userId);
17  return <div>User: {userId}</div>;
18}

Key rules:

  1. Components must be pure functions - same input = same output
  2. Do not mutate props or state directly
  3. Side effects only in useEffect, event handlers, or callbacks
  4. Do not read refs during rendering (only in effects and handlers)

ESLint Plugin

The compiler rules are now part of the official eslint-plugin-react-hooks package (since version 6, currently 7.x), next to rules-of-hooks and exhaustive-deps. You install it with npm install -D eslint-plugin-react-hooks@latest. The recommended preset in the flat config turns on all recommended rules, and you set the level of a single rule in the rules section:

1// eslint.config.js
2import reactHooks from 'eslint-plugin-react-hooks';
3import { defineConfig } from 'eslint/config';
4
5export default defineConfig([
6  {
7    // .js and .jsx files with the JSX parser (without it ESLint 9 skips .jsx files)
8    files: ['**/*.{js,jsx}'],
9    languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
10  },
11  reactHooks.configs.flat.recommended,
12  {
13    rules: {
14      'react-hooks/purity': 'error', // render without side effects, e.g. no Math.random()
15      'react-hooks/refs': 'error', // no reading or writing ref.current during render
16      'react-hooks/immutability': 'error', // no mutating props or state
17    },
18  },
19]);

The old eslint-plugin-react-compiler package with the 'react-compiler/react-compiler' rule has been superseded by these rules, so do not add it to new projects. The linter will also show which components the compiler skips and why.

Migrating Existing Code

If you have an existing project with manual memoization, you can adopt the compiler gradually, for example in annotation mode. Existing React.memo, useMemo and useCallback do not conflict with the compiler. The React documentation recommends leaving them in place or removing them only after testing, because removing them can change the compilation output. In new code rely on the compiler, and treat useMemo and useCallback as an escape hatch when you need precise control, for example over an effect dependency.

Summary

React Compiler 1.0 changes how you optimize React:

  1. Automatic memoization - Compiler memoizes results for you, as if you had added useMemo, useCallback and React.memo where needed
  2. Cleaner code - you write simpler, easier to maintain code
  3. Fewer bugs - Compiler won't forget the dependency array
  4. Build-time - optimizations are created at build time, and only a lightweight cache of results remains at runtime
  5. Requires clean code - components must follow the Rules of React

Remember: React Compiler is like an autopilot on a spaceship - it does its job perfectly, but requires the ship to be properly built.

Code for this lesson: App.jsx
1import React, { useState, useMemo, useCallback, memo } from 'react';
2
3// === REACT COMPILER - Simulation of automatic memoization ===
4// Comparison: manual memoization and "compiled" code
5
6// Render counter: a deliberate side effect in render, for measuring only.
7// In a real app, React DevTools Profiler measures renders.
8let renderLog = {};
9function trackRender(name) {
10  renderLog[name] = (renderLog[name] || 0) + 1;
11  return renderLog[name];
12}
13
14// === VERSION 1: WITHOUT memoization (naive code) ===
15function NaiveMissionCard({ mission, onSelect }) {
16  const count = trackRender('Naive-' + mission.id);
17  return (
18    <div style={{ ...styles.card, borderColor: 'rgba(255,107,107,0.3)' }}>
19      <div style={styles.renderBadge('#ff6b6b')}>R: {count}</div>
20      <strong>{mission.name}</strong>
21      <span style={styles.status(mission.status)}>{STATUS_LABELS[mission.status]}</span>
22      <button onClick={() => onSelect(mission.id)} style={styles.selectBtn}>Select</button>
23    </div>
24  );
25}
26
27// === VERSION 2: MANUAL memoization (React.memo + useCallback + useMemo) ===
28const ManualMissionCard = memo(function ManualMissionCard({ mission, onSelect }) {
29  const count = trackRender('Manual-' + mission.id);
30  return (
31    <div style={{ ...styles.card, borderColor: 'rgba(0,212,255,0.3)' }}>
32      <div style={styles.renderBadge('#00d4ff')}>R: {count}</div>
33      <strong>{mission.name}</strong>
34      <span style={styles.status(mission.status)}>{STATUS_LABELS[mission.status]}</span>
35      <button onClick={() => onSelect(mission.id)} style={styles.selectBtn}>Select</button>
36    </div>
37  );
38});
39
40// === VERSION 3: "Compiled" (React Compiler simulation) ===
41// In a real project the compiler would generate this memoization
42const CompiledMissionCard = memo(function CompiledMissionCard({ mission, onSelect }) {
43  const count = trackRender('Compiled-' + mission.id);
44  return (
45    <div style={{ ...styles.card, borderColor: 'rgba(0,255,136,0.3)' }}>
46      <div style={styles.renderBadge('#00ff88')}>R: {count}</div>
47      <strong>{mission.name}</strong>
48      <span style={styles.status(mission.status)}>{STATUS_LABELS[mission.status]}</span>
49      <button onClick={() => onSelect(mission.id)} style={styles.selectBtn}>Select</button>
50    </div>
51  );
52});
53
54const STATUS_LABELS = { active: 'active', planned: 'planned', completed: 'completed' };
55
56// Test data
57const MISSIONS = [
58  { id: 1, name: 'Alpha Centauri', status: 'active' },
59  { id: 2, name: 'Europa Probe', status: 'planned' },
60  { id: 3, name: 'Mars Colony', status: 'completed' },
61  { id: 4, name: 'Titan Explorer', status: 'active' },
62];
63
64function App() {
65  const [counter, setCounter] = useState(0);
66  const [selected, setSelected] = useState(null);
67  const [filter, setFilter] = useState('all');
68
69  // NAIVE: new function and filtering on every render
70  const naiveFiltered = MISSIONS.filter(m => filter === 'all' || m.status === filter);
71  const naiveSelect = (id) => setSelected(id);
72
73  // MANUAL: useMemo + useCallback
74  const manualFiltered = useMemo(
75    () => MISSIONS.filter(m => filter === 'all' || m.status === filter),
76    [filter]
77  );
78  const manualSelect = useCallback((id) => setSelected(id), []);
79
80  // "COMPILED": the same as manual, but added automatically by the compiler
81  const compiledFiltered = useMemo(
82    () => MISSIONS.filter(m => filter === 'all' || m.status === filter),
83    [filter]
84  );
85  const compiledSelect = useCallback((id) => setSelected(id), []);
86
87  return (
88    <div style={styles.container}>
89      <header style={styles.header}>
90        <h1 style={styles.title}>React Compiler - Comparison</h1>
91        <p style={styles.subtitle}>Click "Re-render" and watch the render counters</p>
92      </header>
93
94      <div style={styles.controls}>
95        <select value={filter} onChange={e => setFilter(e.target.value)} style={styles.select}>
96          <option value="all">All</option>
97          <option value="active">Active</option>
98          <option value="planned">Planned</option>
99          <option value="completed">Completed</option>
100        </select>
101        <button onClick={() => setCounter(c => c + 1)} style={styles.reRenderBtn}>
102          Re-render ({counter})
103        </button>
104        {selected && <span style={styles.selectedInfo}>Selected ID: {selected}</span>}
105      </div>
106
107      <div style={styles.grid}>
108        <div style={styles.column}>
109          <h3 style={{ ...styles.colTitle, color: '#ff6b6b' }}>Without memoization</h3>
110          <p style={styles.colDesc}>Every re-render = new function + new filtering</p>
111          {naiveFiltered.map(m => (
112            <NaiveMissionCard key={m.id} mission={m} onSelect={naiveSelect} />
113          ))}
114        </div>
115
116        <div style={styles.column}>
117          <h3 style={{ ...styles.colTitle, color: '#00d4ff' }}>Manual memoization</h3>
118          <p style={styles.colDesc}>React.memo + useCallback + useMemo</p>
119          {manualFiltered.map(m => (
120            <ManualMissionCard key={m.id} mission={m} onSelect={manualSelect} />
121          ))}
122        </div>
123
124        <div style={styles.column}>
125          <h3 style={{ ...styles.colTitle, color: '#00ff88' }}>React Compiler</h3>
126          <p style={styles.colDesc}>Automatically added memoization</p>
127          {compiledFiltered.map(m => (
128            <CompiledMissionCard key={m.id} mission={m} onSelect={compiledSelect} />
129          ))}
130        </div>
131      </div>
132    </div>
133  );
134}
135
136const styles = {
137  container: { fontFamily: 'system-ui', background: '#0a0e17', color: '#e0e1dd', minHeight: '100vh', padding: '14px' },
138  header: { textAlign: 'center', marginBottom: '12px' },
139  title: { fontSize: '20px', color: '#00d4ff', margin: '0 0 4px' },
140  subtitle: { fontSize: '12px', color: '#8892b0', margin: 0 },
141  controls: { display: 'flex', gap: '8px', justifyContent: 'center', alignItems: 'center', marginBottom: '14px', flexWrap: 'wrap' },
142  select: { padding: '5px 10px', background: '#1a1f2e', color: '#e0e1dd', border: '1px solid rgba(0,212,255,0.3)', borderRadius: '4px', fontSize: '12px' },
143  reRenderBtn: { padding: '5px 14px', background: 'rgba(255,107,107,0.15)', color: '#ff6b6b', border: '1px solid rgba(255,107,107,0.3)', borderRadius: '4px', cursor: 'pointer', fontSize: '12px' },
144  selectedInfo: { fontSize: '12px', color: '#ffd700' },
145  grid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(190px, 1fr))', gap: '10px' },
146  column: { background: 'rgba(0,0,0,0.2)', borderRadius: '8px', padding: '10px' },
147  colTitle: { fontSize: '13px', margin: '0 0 4px' },
148  colDesc: { fontSize: '10px', color: '#556070', margin: '0 0 8px' },
149  card: { display: 'flex', alignItems: 'center', gap: '6px', padding: '6px 8px', background: 'rgba(0,0,0,0.3)', borderRadius: '4px', marginBottom: '4px', fontSize: '11px', border: '1px solid', position: 'relative' },
150  renderBadge: (color) => ({ position: 'absolute', top: '2px', right: '4px', fontSize: '9px', color, fontWeight: 'bold' }),
151  status: (s) => ({ padding: '1px 5px', borderRadius: '6px', fontSize: '9px', background: s === 'active' ? 'rgba(0,255,136,0.15)' : s === 'completed' ? 'rgba(0,212,255,0.15)' : 'rgba(255,170,0,0.15)', color: s === 'active' ? '#00ff88' : s === 'completed' ? '#00d4ff' : '#ffaa00' }),
152  selectBtn: { marginLeft: 'auto', padding: '2px 6px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '3px', color: '#8892b0', cursor: 'pointer', fontSize: '10px' },
153};
154
155export default App;

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What does React Compiler do automatically during compilation?

  2. 2. Which practice BREAKS the Rules of React and may cause errors in React Compiler?

Hands-on tasks in the game

  • Click in order

    Arrange the React Compiler configuration in next.config.js (Next.js 16):

  • Horizontal ordering

    Arrange the ESLint rule that checks component purity - one of the React Compiler rules in eslint-plugin-react-hooks:

Useful articles