React Compiler - How It Works

React Compiler (babel-plugin-react-compiler, stable 1.0 since 7 Oct 2025) is a build-time Babel plugin that rewrites each component and hook so it memoizes its own intermediate…

Published
Reading time
15 min read
React InternalsPart 14 · 15 of 18

What it is, and where it sits in the toolchain

The compiler is "an optimizing compiler that optimizes components and hooks through automatic memoization". It ships as a Babel plugin, but the core is "largely decoupled from Babel": it lowers Babel's AST into its own High-level Intermediate Representation (HIR) and analyses data flow and mutability on that.1 It is a plain dev dependency that runs while you bundle. Nothing extra ships to the browser apart from a tiny runtime import, and it must be the first plugin in your Babel pipeline because it needs the original source shape for its analysis.2 Vite, Next.js (through an swc integration), Expo/Metro, Rsbuild and Rspack all expose it as a switch.23

The same core powers the lint rules in eslint-plugin-react-hooks: the ESLint plugin effectively runs the compiler and reports a subset of its diagnostics.4

The plugin object is named react-forget, the project's old codename, and does all its work from a Program.enter visitor so it sees the code before any other plugin has rewritten it.5 For each function in the file it decides whether this is a component or hook to compile, calls the compiler pipeline, and swaps in the returned AST node.4

Which functions get compiled: compilationMode and directives

getReactFunctionType makes the decision.6

compilationModeCompiles
'infer' (default)Functions named like a component (PascalCase) or hook (useX) that also create JSX or call a hook, plus anything with "use memo"
'annotation'Only functions with a "use memo" directive (good for incremental adoption)
'syntax'Only Flow component/hook syntax
'all'Every top-level function (not recommended)

The opt-in directives are "use memo" / "use forget", and the opt-out directives are "use no memo" / "use no forget".7 Whatever the mode, "use no memo" always wins.8 One consequence of 'infer': plain helper functions such as expensiveSum() below are not compiled. The compiler memoizes the call to them inside a component, but the helper itself is untouched, and its memoization is not shared across components.3

The pipeline (Entrypoint/Pipeline.ts)

runWithEnvironment is the real pass list. It is a long, straight-line function, and every pass logs its output under a name. Those names are what you see in the playground's IR tabs.9 The compiler's own design doc describes it in the same phases.4 Here are the passes that matter, in order:

1. Lowering to HIR: lower()

Lowering turns the Babel AST into a control-flow graph of basic blocks. Each block holds instructions and ends in a terminal. The IR keeps exact JS evaluation order and resolves break/continue targets. It is "high-level" because it still knows the difference between if, ternary and &&, and between for and for..of, so codegen can emit code that looks like what you wrote.1011 Why a CFG and not AST rewriting? Memoization needs data-flow facts ("is x mutated after line 5? on which paths?"), and those are textbook CFG analyses. Early prototypes worked on the AST, and the switch to an HIR is what made precise analysis possible.1

Before SSA there are cleanup passes: pruneMaybeThrows, validateUseMemo, dropManualMemoization (turns your useMemo/useCallback into plain code, keeping markers so it can later check that it preserved your guarantees), IIFE inlining, and block merging.12

2. SSA: enterSSA, eliminateRedundantPhi

Every assignment gets a fresh identifier, and control-flow joins get phi nodes.13 After this pass, "which definition of x reaches this use?" has a single answer, which the later inference passes rely on. constantPropagation follows.

3. Type inference and hook validation: inferTypes, validateHooksUsage

inferTypes is deliberately conservative. It identifies only what matters for later passes: which values are hooks, useState setters, refs, primitives, JSX, and so on.144 Right after it, validateHooksUsage and validateNoCapitalizedCalls run. A conditional hook call fails here.15

4. Effect / aliasing inference: analyseFunctions, inferMutationAliasingEffects, inferMutationAliasingRanges

This is the core of the compiler. inferMutationAliasingEffects interprets every instruction against an abstract heap, recording effects such as Create, Capture, Alias, Mutate and Freeze. Values that flow into JSX or a hook become frozen, and props and hook results are frozen from the start. Mutating a frozen value is the "This value cannot be modified" error.1617 inferMutationAliasingRanges then computes, for each value, its mutable range: the span of instructions from creation to last mutation, extended through aliases.18 The mutable range is the key concept. A value can only be cached as a unit once it is done being built, so everything that mutates it has to sit inside the same memo block.

Then come the render-purity validations: refs read during render, setState called unconditionally in render, locals reassigned after render, and others.19

5. Reactivity: inferReactivePlaces

This pass marks which values can change between renders: props, state, context, hook results, and anything derived from them.20 A useState setter is known to be stable, so it is not reactive. That is why handleSelect in the example below gets a dependency-free, sentinel-only cache slot.

6. Reactive scope construction and merging

  • inferReactiveScopeVariables puts operands that mutate together into disjoint sets. Each set becomes a reactive scope, the future if (...) {...} memo block.21
  • alignReactiveScopesToBlockScopesHIR and mergeOverlappingReactiveScopesHIR stretch and merge scopes so they nest properly inside JS block structure. You cannot emit an if that starts inside one branch and ends in another.22
  • buildReactiveScopeTerminalsHIR makes the scopes explicit in the CFG.
  • flattenScopesWithHooksOrUseHIR removes any scope that contains a hook or use call, because memoizing it would make the hook call conditional.23 This is why useState calls in compiled output always sit outside if ($[i] ...) blocks.
  • propagateScopeDependenciesHIR computes each scope's dependencies, the reactive values it reads (down to property paths like props.items.length), which become the $[i] !== dep checks.24

7. ReactiveFunction and pruning

buildReactiveFunction turns the CFG back into a tree, a hybrid of HIR and AST.25 Then the cost/benefit passes run:

  • pruneNonEscapingScopes: if a value never escapes (it isn't returned, isn't reachable from the return value, isn't passed to a hook), caching it buys nothing, so its scope is removed.26
  • pruneNonReactiveDependencies and pruneUnusedScopes.
  • mergeReactiveScopesThatInvalidateTogether: when scopes have identical dependencies, or one scope's output is the next one's only input, they always invalidate together, so they are merged to save slots and comparisons.27
  • pruneAlwaysInvalidatingScopes: an unmemoized object or array literal is new on every render, so memoizing anything downstream of it is wasted and gets removed.28
  • Then propagateEarlyReturns, promoteUsedTemporaries, renameVariables, and validatePreservedManualMemoization, which checks that it kept the guarantees of any useMemo you wrote.

8. Codegen: codegenFunction

If any slots were used, codegen emits const $ = _c(<count>) at the top.29 For each scope it emits $[i] !== dep comparisons joined with ||.30 A scope with no dependencies gets a one-time init check instead: $[k] === Symbol.for("react.memo_cache_sentinel").31 The import comes from react/compiler-runtime for target '19', or from the react-compiler-runtime npm package for '17'/'18'.32

Real compiler output, annotated

I compiled the example below with babel-plugin-react-compiler@1.0.0 and @babel/core through a small script, with JSX left in place so the result stays readable. The script and the outputs live in the scratchpad (compile-demo/compile.mjs, examples/App.jsx, out-App.js).

Input

JSX
import { useState } from "react";

function expensiveSum(items) {
  let total = 0;
  for (const item of items) total += item.price;
  return total;
}

export function List({ items, onSelect }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id} onClick={() => onSelect(item.id)}>{item.name}</li>
      ))}
    </ul>
  );
}

export function Counter({ items }) {
  const [count, setCount] = useState(0);
  const [selected, setSelected] = useState(null);
  const total = expensiveSum(items);
  const handleSelect = (id) => setSelected(id);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Clicked {count}</button>
      <p>Total: {total} / selected: {selected}</p>
      <List items={items} onSelect={handleSelect} />
    </div>
  );
}

Output for Counter. The code is verbatim; only the // ◀ comments are mine.

JavaScript
import { c as _c } from "react/compiler-runtime";        // ◀ added once per file; = React's useMemoCache
export function Counter(t0) {
  const $ = _c(17);                                        // ◀ 17 slots on this fiber's memo cache
  const {
    items
  } = t0;                                                  // ◀ props destructuring moved into the body
  const [count, setCount] = useState(0);                   // ◀ hooks stay unconditional (flattenScopesWithHooks)
  const [selected, setSelected] = useState(null);
  let t1;
  if ($[0] !== items) {                                    // ◀ scope 1, dep = items (sentinel on mount, so always true)
    t1 = expensiveSum(items);                              // ◀ the "useMemo" you didn't write
    $[0] = items;                                          // ◀ store dep...
    $[1] = t1;                                             // ◀ ...and result (written together: atomic)
  } else {
    t1 = $[1];                                             // ◀ cache hit: skip the loop entirely
  }
  const total = t1;
  let t2;
  if ($[2] === Symbol.for("react.memo_cache_sentinel")) {  // ◀ no reactive deps: setSelected is stable
    t2 = id => setSelected(id);                            // ◀ the "useCallback" you didn't write; created once
    $[2] = t2;
  } else {
    t2 = $[2];
  }
  const handleSelect = t2;
  let t3;
  if ($[3] !== count) {                                    // ◀ inline handler closes over count → dep = count
    t3 = () => setCount(count + 1);
    $[3] = count;
    $[4] = t3;
  } else {
    t3 = $[4];
  }
  let t4;
  if ($[5] !== count || $[6] !== t3) {                     // ◀ <button> element, deps = count and the handler
    t4 = <button onClick={t3}>Clicked {count}</button>;
    $[5] = count;
    $[6] = t3;
    $[7] = t4;
  } else {
    t4 = $[7];
  }
  let t5;
  if ($[8] !== selected || $[9] !== total) {               // ◀ <p> only rebuilt when its inputs change
    t5 = <p>Total: {total} / selected: {selected}</p>;
    $[8] = selected;
    $[9] = total;
    $[10] = t5;
  } else {
    t5 = $[10];
  }
  let t6;
  if ($[11] !== items) {                                   // ◀ <List> element: handleSelect is stable, so dep = items only
    t6 = <List items={items} onSelect={handleSelect} />;
    $[11] = items;
    $[12] = t6;
  } else {
    t6 = $[12];                                            // ◀ SAME element object as last render → see page 15
  }
  let t7;
  if ($[13] !== t4 || $[14] !== t5 || $[15] !== t6) {      // ◀ outer <div>: deps are the child elements
    t7 = <div>{t4}{t5}{t6}</div>;
    $[13] = t4;
    $[14] = t5;
    $[15] = t6;
    $[16] = t7;
  } else {
    t7 = $[16];                                            // ◀ nothing changed → return the previous tree
  }
  return t7;
}

What to take from it:

  • Granularity is finer than you would write by hand. One useMemo per JSX node, keyed on exactly what that node reads. When you click the button, count changes, so t3, t4 and t7 are rebuilt, while t5 and t6 (the <List> element) are reused.
  • Element identity is the payoff. t6 is the identical object from the previous render. That is what lets the reconciler skip List entirely; see With vs Without Compiler.
  • Cache writes are atomic. Inputs and outputs of a scope are written together. React relies on this property in useMemoCache (below).

Output for List. This is also verbatim. Notice the nested scope: the map callback is cached on onSelect alone, and the mapped array on items and onSelect:

JavaScript
export function List(t0) {
  const $ = _c(7);
  const { items, onSelect } = t0;
  let t1;
  if ($[0] !== items || $[1] !== onSelect) {
    let t2;
    if ($[3] !== onSelect) {
      t2 = item => <li key={item.id} onClick={() => onSelect(item.id)}>{item.name}</li>;
      $[3] = onSelect;
      $[4] = t2;
    } else {
      t2 = $[4];
    }
    t1 = items.map(t2);
    $[0] = items; $[1] = onSelect; $[2] = t1;
  } else {
    t1 = $[2];
  }
  // ... <ul>{t1}</ul> cached on t1 (slots 5-6)
}

(Simplified: I collapsed the destructuring and the last scope onto fewer lines.) When a callback captures nothing reactive, the compiler goes further and outlines it to module scope. In the page 15 example, data.map((d) => <Row .../>) becomes data.map(_temp) with function _temp(d) {...} hoisted out.33

The runtime: _c → useMemoCache

On React 19, react/compiler-runtime is a one-line re-export, export {useMemoCache as c} from './ReactHooks', and React also exposes it as React.__COMPILER_RUNTIME.34 useMemoCache is a hook in the dispatcher like any other. The mount and update dispatchers both point at the same function.35 It does not follow the usual mount/update split, and it does not allocate a Hook object in the memoizedState list (see Hooks on Mount for that list). Instead the cache lives on fiber.updateQueue.memoCache.36

JavaScript
// packages/react-reconciler/src/ReactFiberHooks.js (simplified)
function useMemoCache(size) {
  let memoCache = currentlyRenderingFiber.updateQueue?.memoCache;  // re-render in same pass
  if (memoCache == null) {
    const current = currentlyRenderingFiber.alternate;
    const currentMemoCache = current?.updateQueue?.memoCache;
    if (currentMemoCache != null) {
      memoCache = {
        data: enableNoCloningMemoCache
          ? currentMemoCache.data
          : currentMemoCache.data.map(array => array.slice()), // copy-on-write
        index: 0,
      };
    }
  }
  if (memoCache == null) memoCache = { data: [], index: 0 };     // first mount
  // ... ensure updateQueue exists, store memoCache on it
  let data = memoCache.data[memoCache.index];
  if (data === undefined) {
    data = memoCache.data[memoCache.index] = new Array(size);
    for (let i = 0; i < size; i++) data[i] = REACT_MEMO_CACHE_SENTINEL;
  }
  memoCache.index++;
  return data;
}

The full function is in ReactFiberHooks.js.37 A few senior-level details:

  • Mount: the array is filled with Symbol.for('react.memo_cache_sentinel'). Every $[i] !== dep check is then trivially true, because no user value equals that symbol, so every scope computes. Scopes without deps check === sentinel explicitly.
  • Update: copy-on-write. renderWithHooks sets workInProgress.updateQueue = null at the start of every render.38 useMemoCache therefore clones the current fiber's arrays (slice()), unless enableNoCloningMemoCache is on, which it is not in v19.3.1.39 The clone means a render that is interrupted or thrown away can't corrupt the committed cache, which is the same double-buffering idea as fibers (Fiber). The cost is one array copy per compiled component per render.
  • Why atomic writes matter: the comment on that flag explains that because the compiler never records a scope's inputs without its outputs, a future mode could share one cache across interrupted attempts and so "resume" partway through a component.37
  • Render-phase re-renders (setState during render) reuse the work-in-progress cache, and resetFunctionComponentUpdateQueue resets only index, not the data.40
  • The slot count must be constant across renders. In DEV, React prints a console error if it changes.37

React 17/18: react-compiler-runtime

With target: '17' | '18' the import becomes react-compiler-runtime, which you install as a runtime dependency, not a dev dependency.4132 Its c uses React.__COMPILER_RUNTIME.c when present. Otherwise it falls back to a userspace polyfill: useMemo(() => new Array(size).fill($empty), []), a sentinel-filled array kept alive by an ordinary useMemo. The polyfill also sets $[$empty] = true so DevTools can recognise the array.42 The trade-off is that the cache is a regular hook slot rather than living on the update queue, and it is not copy-on-write.

Bailouts: when the compiler leaves a function alone

The compiler's stance is that skipping is always safe. By default (panicThreshold: 'none'), a function that fails validation is emitted unchanged and the build continues.4344 I fed it four known-bad components, and this is what the logger reported (examples/Bailouts.jsx, examples/Mut.jsx):

ComponentResultCompiler event
"use no memo" in bodyLeft as writtenCompileSkip, directive
if (cond) { useState() }Left as writtenCompileError: "Hooks must always be called in a consistent order…"45
return <div>{ref.current}</div>Left as writtenCompileError: "Cannot access refs during render"46
props.label = "x"Left as writtenCompileError: "This value cannot be modified"17
items.push(1) (prop destructured)Compiled!CompileSuccess

Other skip reasons: an ESLint or Flow suppression comment for React rules inside the function (findProgramSuppressions) makes the compiler assume the code breaks the rules.48 Known-incompatible library APIs, such as react-hook-form's watch and TanStack Table, are on a list (DefaultModuleTypeProvider.ts) and cause a skip.49 So do unsupported syntax such as class components, which are an explicit non-goal.50

panicThreshold controls how loud failures are. 'none' (default; the docs say production should always use it) skips silently. 'critical_errors' throws on critical or unrecognised errors. 'all_errors' fails the build on any diagnostic.4351 Config errors always throw.44 To see the skips without failing the build, pass a logger with logEvent (that's what my script does) or use the ESLint rules.

What the compiler does NOT do

  • No reconciler, scheduler, lane or commit changes. It only rewrites your component bodies. Work Loop, beginWork & Reconciliation, Lanes & Scheduler and Commit Phase run the same code with or without it. The speedup comes indirectly, through stable element and prop identities hitting bailouts that already existed.
  • It does not stop a component's function from running when that component has its own state update. It makes the run cheaper.
  • It doesn't compile node_modules (default sources excludes them), class components, or non-component helpers.5250
  • It doesn't share memoization between components or instances. Every fiber has its own cache.3
  • It is not "perfect" memoization. Zero unnecessary recomputation is an explicit non-goal, because tracking overhead and code size would hurt startup.50
  • It doesn't change semantics for code that follows the rules. Existing useMemo/useCallback still work as escape hatches, for example to pin an effect dependency. The docs advise leaving existing manual memoization in place, because removing it can change the compiled output.3

See With vs Without Compiler for the fiber-by-fiber effect on a state update.

Summary

React Compiler is a build-time Babel plugin that rewrites each component and hook to memoize its own values and JSX in a per-fiber array, const $ = _c(n). It lowers your function into a control-flow-graph IR, infers what mutates together and what is reactive, groups that into reactive scopes, and emits one $[i] !== dep check per scope. At runtime it needs only useMemoCache. The reconciler, scheduler and commit phase are untouched; the compiler just hands them stable identities, and the next part shows which bailouts those hit.

Key takeaways

  • compilationMode: 'infer' compiles only component- or hook-named functions that create JSX or call a hook; "use no memo" always wins.
  • HIR lowering: memoization needs data-flow facts, so the AST becomes a control-flow graph of basic blocks.
  • Mutable range: a value is cached as a unit only once it is done being built, so everything that mutates it shares one memo block.
  • inferReactivePlaces: props, state, context and hook results are reactive; a useState setter is stable.
  • flattenScopesWithHooksOrUseHIR: drops scopes containing hooks, which is why useState sits outside the if ($[i] ...) blocks.
  • Codegen: emits $[i] !== dep checks, or a sentinel test for dependency-free scopes, and writes a scope's inputs and outputs together.
  • useMemoCache: keeps the cache on fiber.updateQueue.memoCache, not the hook list, and on update clones it copy-on-write (enableNoCloningMemoCache is off).
  • Bailouts: by default a function that fails validation is emitted unchanged; the compiler catches only what it can prove (items.push(1) compiled).
  • Not fine-grained: React still re-renders top-down from the state owner; re-renders just get cheaper.

← Prev · Index · Next →

Footnotes

  1. React Compiler v1.0 (Lauren Tan, Joe Savona, Mofei Zhang, 7 Oct 2025): https://react.dev/blog/2025/10/07/react-compiler-1 (opens in a new tab) ↩ ↩2

  2. react.dev, "React Compiler: Installation" (must run first in the Babel pipeline): https://react.dev/learn/react-compiler/installation (opens in a new tab) ↩ ↩2

  3. react.dev, "React Compiler: Introduction": https://react.dev/learn/react-compiler/introduction (opens in a new tab) ↩ ↩2 ↩3 ↩4 ↩5

  4. Compiler design doc, Architecture: compiler/docs/DESIGN_GOALS.md#L38-L55 (opens in a new tab) ↩ ↩2 ↩3 ↩4

  5. BabelPluginReactCompiler — compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts#L24-L36 (opens in a new tab) ↩

  6. getReactFunctionType — compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts#L865-L911 (opens in a new tab) ↩

  7. OPT_IN_DIRECTIVES / OPT_OUT_DIRECTIVES — compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts#L49-L50 (opens in a new tab) ↩

  8. react.dev, compilationMode reference: https://react.dev/reference/react-compiler/compilationMode (opens in a new tab) ↩

  9. runWithEnvironment → lower — compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts#L148-L158 (opens in a new tab) ↩

  10. lower — compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts#L72 (opens in a new tab) ↩

  11. Compiler design doc, Design Principles (HIR): compiler/docs/DESIGN_GOALS.md#L29-L36 (opens in a new tab) ↩

  12. Early passes incl. dropManualMemoization — Pipeline.ts#L160-L181 (opens in a new tab); implementation Inference/DropManualMemoization.ts#L391 (opens in a new tab) ↩

  13. enterSSA — compiler/packages/babel-plugin-react-compiler/src/SSA/EnterSSA.ts#L254 (opens in a new tab); called at Pipeline.ts#L188-L198 (opens in a new tab) ↩

  14. inferTypes — compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts#L64 (opens in a new tab) ↩

  15. inferTypes → validateHooksUsage — Pipeline.ts#L200-L212 (opens in a new tab) ↩

  16. analyseFunctions, inferMutationAliasingEffects — Pipeline.ts#L217-L221 (opens in a new tab); implementation Inference/InferMutationAliasingEffects.ts#L98 (opens in a new tab) ↩

  17. "This value cannot be modified" diagnostic — compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts#L612-L615 (opens in a new tab) ↩ ↩2

  18. inferMutationAliasingRanges doc comment — compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts#L32-L75 (opens in a new tab) ↩

  19. Render validations after mutable ranges — Pipeline.ts#L234-L296 (opens in a new tab) ↩

  20. inferReactivePlaces — compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts#L205 (opens in a new tab); called at Pipeline.ts#L298-L299 (opens in a new tab) ↩

  21. inferReactiveScopeVariables — compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts#L60-L86 (opens in a new tab); gated on enableMemoization at Pipeline.ts#L328-L336 (opens in a new tab) ↩

  22. alignReactiveScopesToBlockScopesHIR / mergeOverlappingReactiveScopesHIR — Pipeline.ts#L384-L395 (opens in a new tab); HIR/MergeOverlappingReactiveScopesHIR.ts#L105 (opens in a new tab) ↩

  23. flattenScopesWithHooksOrUseHIR — compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/FlattenScopesWithHooksOrUseHIR.ts#L20-L40 (opens in a new tab) ↩

  24. propagateScopeDependenciesHIR — compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts#L53 (opens in a new tab); called at Pipeline.ts#L428-L433 (opens in a new tab) ↩

  25. buildReactiveFunction — compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/BuildReactiveFunction.ts#L42 (opens in a new tab); pruning passes at Pipeline.ts#L435-L491 (opens in a new tab) ↩

  26. pruneNonEscapingScopes doc comment — compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts#L40-L60 (opens in a new tab) ↩

  27. mergeReactiveScopesThatInvalidateTogether doc comment — compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts#L60-L86 (opens in a new tab) ↩

  28. pruneAlwaysInvalidatingScopes — compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneAlwaysInvalidatingScopes.ts#L18-L29 (opens in a new tab) ↩

  29. codegenFunction emits const $ = _c(n) — compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts#L163-L178 (opens in a new tab) ↩

  30. codegenReactiveScope builds $[i] !== dep — CodegenReactiveFunction.ts#L560-L590 (opens in a new tab) ↩

  31. Sentinel test for dependency-free scopes — CodegenReactiveFunction.ts#L650-L667 (opens in a new tab); constant MEMO_CACHE_SENTINEL at L62 (opens in a new tab) ↩

  32. getReactCompilerRuntimeModule — compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts#L1337-L1345 (opens in a new tab) ↩ ↩2

  33. enableFunctionOutlining → outlineFunctions — Pipeline.ts#L358-L361 (opens in a new tab) ↩

  34. react/compiler-runtime → packages/react/src/ReactCompilerRuntime.js#L10 (opens in a new tab); __COMPILER_RUNTIME export packages/react/src/ReactClient.js#L111 (opens in a new tab); useMemoCache → dispatcher packages/react/src/ReactHooks.js#L212-L216 (opens in a new tab) ↩

  35. HooksDispatcherOnMount / OnUpdate both use useMemoCache — packages/react-reconciler/src/ReactFiberHooks.js#L3926-L3981 (opens in a new tab) ↩

  36. FunctionComponentUpdateQueue.memoCache — packages/react-reconciler/src/ReactFiberHooks.js#L249-L254 (opens in a new tab) ↩

  37. useMemoCache — packages/react-reconciler/src/ReactFiberHooks.js#L1181-L1263 (opens in a new tab) ↩ ↩2 ↩3

  38. renderWithHooks clears updateQueue — packages/react-reconciler/src/ReactFiberHooks.js#L529-L531 (opens in a new tab) ↩

  39. enableNoCloningMemoCache = false — packages/shared/ReactFeatureFlags.js#L128 (opens in a new tab) ↩

  40. resetFunctionComponentUpdateQueue — packages/react-reconciler/src/ReactFiberHooks.js#L1084-L1096 (opens in a new tab) ↩

  41. react.dev, target reference: https://react.dev/reference/react-compiler/target (opens in a new tab) ↩

  42. react-compiler-runtime c polyfill — compiler/packages/react-compiler-runtime/src/index.ts#L19-L39 (opens in a new tab) ↩

  43. react.dev, panicThreshold reference: https://react.dev/reference/react-compiler/panicThreshold (opens in a new tab) ↩ ↩2

  44. handleError (panicThreshold) — compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts#L246-L262 (opens in a new tab) ↩ ↩2

  45. validateHooksUsage — compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateHooksUsage.ts#L86 (opens in a new tab) ↩

  46. validateNoRefAccessInRender — compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts#L122 (opens in a new tab) ↩

  47. react.dev, "Debugging and Troubleshooting": https://react.dev/learn/react-compiler/debugging (opens in a new tab) ↩

  48. findProgramSuppressions usage — compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts#L431-L443 (opens in a new tab) ↩

  49. react.dev, incompatible-library lint: https://react.dev/reference/eslint-plugin-react-hooks/lints/incompatible-library (opens in a new tab); known list incl. react-hook-form, @tanstack/react-table in compiler/packages/babel-plugin-react-compiler/src/HIR/DefaultModuleTypeProvider.ts#L46-L89 (opens in a new tab) ↩

  50. Compiler design doc, Goals / Non-Goals: compiler/docs/DESIGN_GOALS.md#L5-L27 (opens in a new tab) ↩ ↩2 ↩3

  51. PanicThresholdOptionsSchema — compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts#L20-L37 (opens in a new tab) ↩

  52. defaultOptions (compilationMode: 'infer', panicThreshold: 'none', sources excludes node_modules, target: '19') — compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts#L312-L330 (opens in a new tab) ↩