React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

react-compilerreact-forgetautomatic-memoizationbabel-pluginperformancereact-19

React Compiler (React Forget) — Automatic memoization at build time

Recipe

// Step 1: Install the compiler
// npm install -D babel-plugin-react-compiler
 
// Step 2: Add to your Next.js config (next.config.ts)
const nextConfig = {
  experimental: {
    reactCompiler: true,
  },
};
 
export default nextConfig;
 
// Step 3: Remove manual memoization — the compiler handles it
// BEFORE: Manual memoization
function ProductList({ products, category }: Props) {
  const filtered = useMemo(
    () => products.filter((p) => p.category === category),
    [products, category]
  );
  const handleClick = useCallback((id: string) => {
    selectProduct(id);
  }, [selectProduct]);
 
  return <MemoizedGrid items={filtered} onClick={handleClick} />;
}
const MemoizedGrid = memo(Grid);
 
// AFTER: Compiler auto-memoizes — cleaner code, same performance
function ProductList({ products, category }: Props) {
  const filtered = products.filter((p) => p.category === category);
  const handleClick = (id: string) => selectProduct(id);
 
  return <Grid items={filtered} onClick={handleClick} />;
}

When to reach for this: When starting a new React 19+ project or migrating an existing one. The compiler eliminates boilerplate from useMemo, useCallback, and memo while maintaining equivalent or better performance.

Working Example

// ---- BEFORE: Manual memoization with 15 lines of boilerplate ----
 
import { memo, useMemo, useCallback, useState } from "react";
 
interface User {
  id: string;
  name: string;
  role: "admin" | "user";
  lastActive: Date;
}
 
function UserDashboard({ users }: { users: User[] }) {
  const [search, setSearch] = useState("");
  const [roleFilter, setRoleFilter] = useState<"all" | "admin" | "user">("all");
  const [selectedId, setSelectedId] = useState<string | null>(null);
 
  const filteredUsers = useMemo(
    () =>
      users
        .filter((u) => roleFilter === "all" || u.role === roleFilter)
        .filter((u) => u.name.toLowerCase().includes(search.toLowerCase()))
        .sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime()),
    [users, roleFilter, search]
  );
 
  const handleSelect = useCallback((id: string) => {
    setSelectedId(id);
  }, []);
 
  const stats = useMemo(
    () => ({
      total: users.length,
      admins: users.filter((u) => u.role === "admin").length,
      active: users.filter((u) => {
        const hourAgo = Date.now() - 3600_000;
        return u.lastActive.getTime() > hourAgo;
      }).length,
    }),
    [users]
  );
 
  return (
    <div>
      <MemoizedStatsBar stats={stats} />
      <input value={search} onChange={(e) => setSearch(e.target.value)} />
      <select
        value={roleFilter}
        onChange={(e) => setRoleFilter(e.target.value as "all" | "admin" | "user")}
      >
        <option value="all">All</option>
        <option value="admin">Admin</option>
        <option value="user">User</option>
      </select>
      <MemoizedUserList
        users={filteredUsers}
        selectedId={selectedId}
        onSelect={handleSelect}
      />
    </div>
  );
}
 
const MemoizedStatsBar = memo(function StatsBar({
  stats,
}: {
  stats: { total: number; admins: number; active: number };
}) {
  return (
    <div className="flex gap-4">
      <span>Total: {stats.total}</span>
      <span>Admins: {stats.admins}</span>
      <span>Active: {stats.active}</span>
    </div>
  );
});
 
const MemoizedUserList = memo(function UserList({
  users,
  selectedId,
  onSelect,
}: {
  users: User[];
  selectedId: string | null;
  onSelect: (id: string) => void;
}) {
  return (
    <ul>
      {users.map((user) => (
        <li
          key={user.id}
          onClick={() => onSelect(user.id)}
          className={user.id === selectedId ? "bg-blue-100" : ""}
        >
          {user.name} ({user.role})
        </li>
      ))}
    </ul>
  );
});
 
// ---- AFTER: React Compiler — same performance, zero boilerplate ----
 
function UserDashboard({ users }: { users: User[] }) {
  const [search, setSearch] = useState("");
  const [roleFilter, setRoleFilter] = useState<"all" | "admin" | "user">("all");
  const [selectedId, setSelectedId] = useState<string | null>(null);
 
  const filteredUsers = users
    .filter((u) => roleFilter === "all" || u.role === roleFilter)
    .filter((u) => u.name.toLowerCase().includes(search.toLowerCase()))
    .sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime());
 
  const stats = {
    total: users.length,
    admins: users.filter((u) => u.role === "admin").length,
    active: users.filter((u) => {
      const hourAgo = Date.now() - 3600_000;
      return u.lastActive.getTime() > hourAgo;
    }).length,
  };
 
  return (
    <div>
      <StatsBar stats={stats} />
      <input value={search} onChange={(e) => setSearch(e.target.value)} />
      <select
        value={roleFilter}
        onChange={(e) => setRoleFilter(e.target.value as "all" | "admin" | "user")}
      >
        <option value="all">All</option>
        <option value="admin">Admin</option>
        <option value="user">User</option>
      </select>
      <UserList
        users={filteredUsers}
        selectedId={selectedId}
        onSelect={(id: string) => setSelectedId(id)}
      />
    </div>
  );
}
 
function StatsBar({ stats }: { stats: { total: number; admins: number; active: number } }) {
  return (
    <div className="flex gap-4">
      <span>Total: {stats.total}</span>
      <span>Admins: {stats.admins}</span>
      <span>Active: {stats.active}</span>
    </div>
  );
}
 
function UserList({
  users,
  selectedId,
  onSelect,
}: {
  users: User[];
  selectedId: string | null;
  onSelect: (id: string) => void;
}) {
  return (
    <ul>
      {users.map((user) => (
        <li
          key={user.id}
          onClick={() => onSelect(user.id)}
          className={user.id === selectedId ? "bg-blue-100" : ""}
        >
          {user.name} ({user.role})
        </li>
      ))}
    </ul>
  );
}

What this demonstrates:

  • The compiler-optimized version removes 6 hook calls (useMemo x2, useCallback x1, memo x2 wrappers) and 15 lines of boilerplate
  • Performance is equivalent because the compiler inserts memoization at the AST level during build
  • Code is more readable and maintainable without manual dependency arrays
  • No behavior changes — the compiler only adds optimizations, never changes semantics

Deep Dive

How It Works

  • Build-time transformation — The React Compiler is a Babel plugin that analyzes your component code at build time. It identifies values, functions, and JSX elements that can be safely cached between renders.
  • Automatic dependency tracking — The compiler statically analyzes which variables each expression depends on, then wraps those expressions in memoization with the correct dependency arrays. This is more reliable than manually writing dependency arrays.
  • Component-level memoization — The compiler wraps component return values so that if props and state have not changed, the component skips rendering entirely. This replaces React.memo.
  • Fine-grained caching — Unlike manual useMemo which caches at the granularity you specify, the compiler can cache individual JSX elements within a component, potentially achieving better granularity than hand-written optimization.
  • Rules of React enforcement — The compiler assumes your code follows the Rules of React (pure rendering, no mutation during render, stable hook call order). Code that violates these rules may produce incorrect output when compiled.

Variations

Opting out with the "use no memo" directive:

// Opt a specific component out of compiler optimization
function DebugPanel({ data }: { data: unknown }) {
  "use no memo";
  // This component always re-renders — useful for debugging
  console.log("DebugPanel rendered at", Date.now());
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

Per-file opt-in (gradual migration):

// next.config.ts — only compile specific directories
const nextConfig = {
  experimental: {
    reactCompiler: {
      compilationMode: "annotation",
      // Or target specific paths:
      // include: ["src/components/**"],
    },
  },
};
// Opt in at the component level with "use memo"
function ExpensiveComponent({ data }: Props) {
  "use memo";
  // Compiler optimizes this component
  return <Chart data={data} />;
}

Checking compiler output:

# See what the compiler transforms
npx react-compiler-healthcheck
 
# Or inspect the compiled output
npx babel --plugins babel-plugin-react-compiler src/Component.tsx

TypeScript Notes

  • The compiler works with TypeScript out of the box. No additional type configuration is needed.
  • Type annotations are preserved — the compiler only transforms runtime behavior.
  • Generic components are supported. The compiler handles them the same as non-generic components.
  • The "use no memo" directive is a string literal and does not need a type declaration.

Gotchas

  • External mutable state breaks compiler assumptions — The compiler assumes pure rendering. If a component reads from a mutable global variable or a ref during render, the compiler may cache a stale result. Fix: Use useSyncExternalStore for external stores, or add the "use no memo" directive.

  • Side effects during render — Code that logs, mutates external objects, or triggers network requests during render will be cached by the compiler, meaning side effects may not run on every render as expected. Fix: Move side effects into useEffect or event handlers.

  • Non-idempotent computations — If a value computation relies on Date.now(), Math.random(), or other non-deterministic sources, the compiler may cache a stale result. Fix: Move non-deterministic values into state or effects.

  • Class components are not optimized — The compiler only works with function components and hooks. Class components are passed through unchanged. Fix: Migrate class components to function components.

  • Manual memo is still needed in rare cases — The compiler may not optimize cross-component boundaries in all cases, particularly with higher-order components or render props. Fix: Profile after enabling the compiler. Add manual memo only where profiling shows the compiler missed an optimization.

  • Keep existing manual memoization during migration — Removing useMemo/useCallback before enabling the compiler leaves your app unoptimized. Fix: Enable the compiler first, verify performance is maintained, then remove manual memoization.

Alternatives

ApproachTrade-off
Manual useMemo + useCallback + memoFull control; verbose; error-prone dependency arrays
React CompilerZero boilerplate; requires React 19+; experimental
State colocationAvoids the need for memoization entirely; may need restructuring
Zustand selectorsFine-grained subscriptions; adds external dependency
Million.jsAlternative compiler for virtual DOM optimization; third-party
Signals (Preact, Solid)Reactive model avoids re-render problem; different framework

FAQs

What is the React Compiler and how does it differ from manual memoization?

The React Compiler is a Babel plugin that analyzes component code at build time and automatically inserts memoization (equivalent to useMemo, useCallback, and memo). Unlike manual memoization, you do not write dependency arrays -- the compiler tracks dependencies statically.

How do you enable the React Compiler in a Next.js project?
// next.config.ts
const nextConfig = {
  experimental: {
    reactCompiler: true,
  },
};
export default nextConfig;

Also install the Babel plugin: npm install -D babel-plugin-react-compiler.

Can you opt a specific component out of compiler optimization?

Yes. Add the "use no memo" directive at the top of the component body:

function DebugPanel({ data }: { data: unknown }) {
  "use no memo";
  console.log("Rendered at", Date.now());
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
What are the Rules of React that the compiler assumes you follow?
  • Pure rendering: no side effects during render
  • No mutation of props or state during render
  • Stable hook call order (no hooks inside conditions or loops)
  • Idempotent render output for the same inputs

Violating these rules may produce incorrect cached output.

Gotcha: What happens if a component reads from a mutable global variable during render?

The compiler may cache a stale result because it assumes pure rendering. If a component reads from window.someGlobal or a mutable ref during render, the cached output will not reflect updates to that global.

Fix: Use useSyncExternalStore for external stores, or add "use no memo".

Gotcha: Why might side effects during render stop firing after enabling the compiler?

The compiler caches component output. If side effects (logging, network requests, mutations) run during render, they may be skipped when the cached result is reused.

Fix: Move side effects into useEffect or event handlers.

Does the compiler work with class components?

No. The compiler only optimizes function components and hooks. Class components are passed through unchanged. Migrate class components to function components to benefit.

Should you remove existing useMemo and useCallback before enabling the compiler?

No. Enable the compiler first, verify performance is maintained, then remove manual memoization. Removing hooks before enabling the compiler leaves your app unoptimized during the gap.

Does the React Compiler require any special TypeScript configuration?

No. The compiler works with TypeScript out of the box. Type annotations are preserved -- the compiler only transforms runtime behavior. Generic components are supported without changes.

How can you check what the compiler transforms in your code?
npx react-compiler-healthcheck
# Or inspect compiled output directly:
npx babel --plugins babel-plugin-react-compiler src/Component.tsx
What does "use memo" do in annotation compilation mode?

In compilationMode: "annotation" mode, only components with the "use memo" directive are optimized by the compiler. This allows gradual migration by opting in per component.

How does the compiler's fine-grained caching differ from manual useMemo?

Manual useMemo caches at the granularity you specify (one value per hook call). The compiler can cache individual JSX elements within a component, potentially achieving better granularity than hand-written optimization.

  • Memoization — Manual useMemo, useCallback, and React.memo patterns
  • Preventing Re-renders — Structural patterns that reduce re-renders without memoization
  • Profiling — Verifying that the compiler is effectively optimizing your components
  • Bundle Optimization — Compiler adds minimal bundle overhead