facebook/react
github.com/facebook/react ↗ · 4000 files · 24.3 MB · scan capped
React · React is a JavaScript library for building user interfaces.
Overview
This is the facebook/react monorepo at commit 59aff3e — the React runtime (isomorphic `react`, the Fiber reconciler, the Fizz SSR server, the Flight/Server-Components client+server, DevTools, and the Scheduler) plus a largely independent `compiler/` sub-project holding the React Compiler in both TypeScript (`babel-plugin-react-compiler`) and an in-progress Rust port (12 `react_compiler_*` crates behind a napi bridge). It is a library-publishing repo, not an app: nearly every artifact is a Rollup-built npm bundle produced per *release channel* (stable / canary / experimental / www-classic / www-modern / xplat), with behaviour toggled by ~67 `enable*` feature flags that are fork-substituted at build time. What is distinctive is the fork/shim architecture — `ReactFiberConfig`, `ReactFeatureFlags`, `ReactFlightServerConfig` and friends are placeholder modules that *throw* unless Rollup or Jest swaps in a renderer-specific implementation — and a CI matrix that runs the same Jest suite 18 ways × 5 shards to prove every channel behaves.
System map
There is no single entrypoint; the repo is a set of publishable packages under `packages/` (workspace glob in `package.json`) plus a nested Yarn/Cargo workspace under `compiler/`. The public runtime entrypoints are `packages/react/index.js` (isomorphic), `packages/react-dom/client.js` → `ReactDOMRoot.createRoot`, `packages/react-dom/server.node.js` → Fizz, and `packages/react-server-dom-*/server.*` → Flight. Everything is compiled by `scripts/rollup/build.js`, driven by a bundle table in `scripts/rollup/bundles.js` and a module-substitution table in `scripts/rollup/forks.js`; `build-all-release-channels.js` runs the whole thing once per channel and rewrites versions from `ReactVersions.js`. There is no database or persistent storage layer — the only 'storage' is the Fiber tree in memory, npm as the deployment target, and GitHub Actions artifacts. Tests never run `jest` directly; `scripts/jest/jest-cli.js` picks one of six configs and spawns Jest with channel/env/variant globals injected.
- `package.json` declares a Yarn-1 workspace over `packages/*` with `packageManager: yarn@1.22.22`, and blocks direct Jest invocation via `"jest": {"testRegex": "/scripts/jest/dont-run-jest-directly\\.js$"}` — all testing must go through `yarn test` → `scripts/jest/jest-cli.js`.
- `ReactVersions.js` is the single source of truth for publishing: `ReactVersion = '19.3.0'`, a `stablePackages` map (react-reconciler 0.34.0, scheduler 0.28.0, react-refresh 0.19.0, eslint-plugin-react-hooks 7.1.1) and `experimentalPackages = ['react-markup']`; a package not listed there is never published.
- The client entrypoint is `createRoot(container, options)` in `packages/react-dom/src/client/ReactDOMRoot.js`, which calls `createContainer(..., ConcurrentRoot, ...)`, `markContainerAsRoot`, then `listenToAllSupportedEvents(rootContainerElement)` before returning a `ReactDOMRoot` wrapper whose `.render` delegates to `updateContainer`.
- The reconciler's public surface is `packages/react-reconciler/src/ReactFiberReconciler.js` — `createContainer`, `updateContainer`, `updateContainerSync`, and `updateContainerImpl` which builds an update via `createUpdate(lane)` / `enqueueUpdate` and hands off to `scheduleUpdateOnFiber(root, rootFiber, lane)`.
- The renderer boundary is the `ReactFiberConfig` shim: `packages/react-reconciler/src/ReactFiberConfig.js` contains only `throw new Error('This module must be shimmed by a specific renderer.')` — Rollup (`scripts/rollup/forks.js`) and Jest (`scripts/jest/setupHostConfigs.js`) both substitute a real host config per renderer.
- `scripts/shared/inlinedHostConfigs.js` enumerates ~20 renderer targets by `shortName` (`dom-browser`, `dom-node`, `dom-edge-webpack`, `dom-node-turbopack`, `dom-node-parcel`, `dom-bun`, `fabric`, `markup`, `dom-fb`, …), each with an `entryPoints` list — this table is the authoritative list of runtime boundaries.
- Cooperative scheduling lives in `packages/scheduler/src/forks/Scheduler.js`: `unstable_scheduleCallback` pushes into a min-heap, `workLoop` drains it, and `shouldYieldToHost` yields once `timeElapsed >= frameInterval` (default `frameYieldMs`, tunable via an fps setter); the host callback is scheduled via `setImmediate` when available, else `MessageChannel` (chosen to dodge the 4 ms `setTimeout` clamp), else `setTimeout`.
- Builds are orchestrated by `scripts/rollup/build.js` (`buildEverything` → `createBundle` per bundle × bundleType, with Babel, Google Closure Compiler for prod bundles, and `rollup-plugin-prettier` for FB/dev output) over the ~20 `bundleTypes` declared in `scripts/rollup/bundles.js` (NODE_DEV/PROD/PROFILING, ESM_*, BUN_*, FB_WWW_*, RN_OSS_*, RN_FB_*, BROWSER_SCRIPT, CJS_DTS/ESM_DTS).
- `scripts/rollup/build-all-release-channels.js` shells out to the build once per channel via `spawnSync`, then `processStable(buildDir)` / `processExperimental(buildDir)` rewrite package versions using the short SHA from `git rev-parse HEAD`, merging the experimental tree into the stable one.
- The compiler is a second workspace: `compiler/Cargo.toml` defines a Rust workspace over `crates/*` plus `packages/babel-plugin-react-compiler-rust/native`, pinned to edition 2024 / rust-version 1.85, version `0.0.1-oidc.0`, with a `[profile.release]` of fat LTO + 1 codegen unit + strip (documented as shrinking the shipped `index.node` from 11.2 MB to 7.2 MB).
- The Rust compiler is reached from JS through napi: `native/src/lib.rs` exposes `#[napi] pub fn compile(ast_json, scope_json, options_json) -> String`, spawning a dedicated 64 MB-stack thread and catching panics; `src/bridge.ts` loads it with `require('../native')` and passes JSON.
- External integrations are narrow and mostly tooling-side: npm registry (`scripts/release/publish.js`, `scripts/release/ci-npmrc`), Discord webhooks in four notify workflows, Vercel for the compiler playground (`compiler/apps/playground/vercel.json`), and Algolia's `beta-react` index queried by the React MCP server.
Feature catalog
User-facing surface is API surface, not screens. `packages/react/src/ReactClient.js` is the canonical export list for the isomorphic package; `react-dom` fans out to client, server (Node/Edge/Browser/Bun), static prerender and test-utils entrypoints; `react-server-dom-{webpack,turbopack,parcel,esm,unbundled,fb}` each ship a bundler-specific Flight client+server pair. Tooling packages are first-class features too: `eslint-plugin-react-hooks` (now also re-exporting compiler-derived rules), `react-refresh` (Fast Refresh Babel plugin + runtime), DevTools in five packaging flavours, and two MCP servers. Behaviour is further gated by ~67 `enable*` flags in `packages/shared/ReactFeatureFlags.js`, inspectable via `yarn flags`.
- The isomorphic `react` package exports the full modern surface including `use`, `cache`, `cacheSignal`, `Activity`, `ViewTransition`, `addTransitionType`, `unstable_SuspenseList`, `unstable_TracingMarker`, and `__COMPILER_RUNTIME` — enumerated in `packages/react/index.js` and implemented in `ReactClient.js`.
- Hooks are exported from `packages/react/src/ReactHooks.js` and include the newer `useOptimistic`, `useActionState`, `useEffectEvent`, `useSyncExternalStore`, `useDeferredValue`, `useCacheRefresh` and `use`.
- `react-dom` ships eleven distinct entrypoints — `client.js`, `server.js`/`server.node.js`/`server.edge.js`/`server.browser.js`/`server.bun.js`, `static.js`/`static.node.js`/`static.edge.js`/`static.browser.js`, `profiling.js`, `test-utils.js`, `unstable_testing.js` and `unstable_server-external-runtime.js`.
- Streaming SSR is `renderToPipeableStream` in `packages/react-dom/src/server/ReactDOMFizzServerNode.js`, which builds a request via `createRequestImpl` → `createRequest`, kicks off `startWork(request)`, and exposes `pipe(destination)` that calls `startFlowing(request, destination)`.
- Server Actions are a shipped feature: `decodeAction(body, serverManifest)` scans FormData for `$ACTION_REF_*` / `$ACTION_ID_*` keys, resolves the reference through `loadServerReference`, and returns the action pre-bound to the remaining FormData; `decodeFormState` handles `$ACTION_KEY` for `useActionState`.
- Flight (RSC) supports multiple request body shapes on the server: `decodeReply`, `decodeReplyFromBusboy` (multipart uploads) and `decodeReplyFromAsyncIterable`, all exported from `ReactFlightDOMServerNode.js`.
- Taint APIs `taintUniqueValue` and `taintObjectReference` guard against leaking server secrets to the client, backed by `TaintRegistryObjects` (WeakMap), `TaintRegistryValues` (refcounted Map) and a `FinalizationRegistry` cleanup hook; gated by `enableTaint = __EXPERIMENTAL__`.
- `eslint-plugin-react-hooks` v7 ships `rules-of-hooks` and `exhaustive-deps` plus a set of compiler-derived rules merged in from `shared/ReactCompiler`, with `recommended` and `recommendedLatest` flat-config presets and a deprecated-rule shim for `component-hook-factories`.
- Fast Refresh is two artifacts: `ReactFreshBabelPlugin.js` (build-time instrumentation) and `ReactFreshRuntime.js` (runtime registry / signature checking), published as `react-refresh@0.19.0`.
- DevTools ships in five packaging flavours — `react-devtools` (standalone), `-core`, `-extensions` (browser extension), `-inline`, `-fusebox` (React Native / Chrome DevTools frontend) — all sharing `react-devtools-shared`, plus `react-devtools-shell` for local development.
- `react-devtools-cdt-mcp` is a browser library that registers React inspection and profiling tools with `chrome-devtools-mcp`, published from its own workflow.
- A second MCP server lives in the compiler: `compiler/packages/react-mcp-server` registers tools including `query-react-dev-docs` (Algolia-backed react.dev search) and a runtime-performance tool driven by headless Puppeteer.
Data flow
On the client, input enters through DOM events (delegated at the root by `listenToAllSupportedEvents`) or imperative `root.render`, both of which funnel into `scheduleUpdateOnFiber(root, fiber, lane)`. State lives on the Fiber tree hanging off `FiberRoot`; the `lane` bitmask on `ReactFiberLane.js` is the priority currency that decides whether work is flushed synchronously, in a microtask, or via a Scheduler task. Output boundaries are the host config (`ReactFiberConfig`, substituted per renderer) and, for SSR/RSC, the stream `Destination` in Fizz/Flight. Cross-module state is deliberately routed through a single mutable singleton, `ReactSharedInternals` (`.H` = current hooks dispatcher, `.T` = current transition, `.A` = async dispatcher), which is itself a forked module so client and server get different instances.
- All client updates converge on `scheduleUpdateOnFiber`, which marks the root (`markRootUpdated(root, lane)`), merges interleaved/render-phase lanes, and calls `ensureRootIsScheduled(root)`; sync updates on legacy roots additionally trigger `flushSyncWorkOnLegacyRootsOnly()` immediately.
- `ensureRootIsScheduled` does not schedule work directly — it appends the root to an intrusive linked list (`firstScheduledRoot`/`lastScheduledRoot` via `root.next`), sets `mightHavePendingSyncWork = true`, and defers to a microtask that runs `processRootScheduleInMicrotask` → `scheduleTaskForRootDuringMicrotask`.
- Priority is encoded as a 31-bit lane bitmask (`TotalLanes = 31`) with named lanes `SyncLane`, `InputContinuousLane`, `DefaultLane`, transition lanes, retry lanes, `IdleLane`, `OffscreenLane` and `DeferredLane`; `requestUpdateLane(fiber)` picks the lane for each update.
- `performWorkOnRoot` decides between `renderRootConcurrent` and `renderRootSync` from `shouldTimeSlice`, which is false for blocking lanes, expired lanes, or `forceSync` — but forced true when `checkIfRootIsPrerendering(root, lanes)` so prerendering never blocks the main thread.
- External-store consistency is re-validated after a concurrent render: if `isRenderConsistentWithExternalStores(finishedWork)` fails, `performWorkOnRoot` discards the result and re-runs `renderRootSync(root, lanes, false)` to block further mutation.
- Hook state is read and written through a swappable dispatcher on the shared-internals singleton: `renderWithHooks` assigns `ReactSharedInternals.H` to `HooksDispatcherOnMount` or `HooksDispatcherOnUpdate` (DEV variants included) and resets it to `ContextOnlyDispatcher` when the component finishes.
- `root.render(children)` writes nothing directly — it calls `updateContainer(children, root, null, null)`, which becomes an update object on the host-root fiber's update queue via `createUpdate(lane)` + `enqueueUpdate(rootFiber, update, lane)`.
- DOM events are the other input boundary and are delegated once per root: `createRoot` and `hydrateRoot` each call `listenToAllSupportedEvents(container)` from `react-dom-bindings/src/events/DOMPluginEventSystem`, so per-node listeners are not attached.
- On the server, Fizz's data flow is request-scoped: `createRequest(...)` builds a `Request`, `startWork(request)` renders tasks (`retryTask`, `renderNode`, `renderNodeDestructive`), and `startFlowing(request, destination)` drains completed segments through `flushCompletedQueues` into the stream.
- Server-Action input crosses a trust boundary as untyped `FormData`: `decodeAction` copies every non-`$ACTION_` field into a fresh `FormData`, decodes bound args by replaying them through `createResponse`/`getRoot` in `decodeBoundActionMetaData`, and binds the result — so client-supplied bytes drive `resolveServerReference(bundlerConfig, id)`.
- `decodeBoundActionMetaData` accepts an `arraySizeLimit` parameter threaded into `createResponse`, indicating the Flight reply parser has an explicit bound on attacker-controlled array sizes.
- DevTools data flows over a batched message bus: `Bridge` wraps a `Wall`, queues events and flushes them on a `BATCH_DURATION` timer, sending typed `BackendEvents` (`overrideProps`, `overrideHookState`, `overrideState`, `overrideContext`) from frontend to backend.
Behavior traces
Three flows are worth walking: (1) client startup/first paint, from `createRoot` through the microtask scheduler into render and commit; (2) the compile flow, where Babel hands a function to either the TS `Pipeline.ts` or the Rust `pipeline.rs`; (3) the release flow, where a commit becomes npm artifacts. The commit phase in particular is a fixed three-pass sequence with `root.current = finishedWork` as the atomic swap point between mutation and layout, and passive effects flushed later. Each trace below names the functions in call order with line anchors.
- Startup trace: `createRoot(container, options)` → validate via `isValidContainer` → `createContainer(container, ConcurrentRoot, …, onUncaughtError, onCaughtError, onRecoverableError)` → `markContainerAsRoot(root.current, container)` → `listenToAllSupportedEvents(rootContainerElement)` → `new ReactDOMRoot(root)`.
- First render trace: `root.render(children)` → `updateContainer` → `updateContainerImpl` → `requestUpdateLane(current)` → `createUpdate(lane)` → `enqueueUpdate(rootFiber, update, lane)` → `scheduleUpdateOnFiber(root, rootFiber, lane)`.
- Scheduling trace: `scheduleUpdateOnFiber` → `markRootUpdated(root, lane)` → `ensureRootIsScheduled(root)` → `ensureScheduleIsScheduled()` → `scheduleImmediateRootScheduleTask()` → (microtask) `processRootScheduleInMicrotask` → `scheduleTaskForRootDuringMicrotask` → `Scheduler_scheduleCallback(priorityLevel, performWorkOnRootViaSchedulerTask)`.
- Render trace: `performWorkOnRoot(root, lanes, forceSync)` computes `shouldTimeSlice`, dispatches to `renderRootConcurrent` (line 2779) or `renderRootSync` (line 2623), then loops on the `RootExitStatus` — handling `RootInProgress` (yield, `startYieldTimer`), store inconsistency (re-render sync), and `RootErrored` (`getLanesToRetrySynchronouslyOnError` → `recoverFromConcurrentError`).
- Component render trace: for each function component the reconciler calls `renderWithHooks`, which installs the mount-or-update dispatcher on `ReactSharedInternals.H`, invokes the component, then restores `ContextOnlyDispatcher`; `bailoutHooks` is the memoized fast path.
- Commit trace: `commitRoot` (line 3725) → `commitBeforeMutationEffects(root, finishedWork, lanes)` → `commitMutationEffects(root, finishedWork, lanes)` → `root.current = finishedWork` (the tree swap, line 4051) → `commitLayoutEffects(finishedWork, root, lanes)` → `flushSpawnedWork()`; passive effects run later via `flushPendingEffects` → `commitPassiveMountEffects`.
- Yield trace: inside a Scheduler task, `workLoop(initialTime)` drains the min-heap and stops when `shouldYieldToHost()` returns true (elapsed ≥ `frameInterval`); the continuation is re-armed by `schedulePerformWorkUntilDeadline()` through `setImmediate`/`MessageChannel`/`setTimeout`.
- SSR trace: `renderToPipeableStream(children, options)` → `createRequestImpl` → `createRequest(...)` → `startWork(request)` (line 6370) → tasks retried through `retryTask`/`renderNode` → consumer calls `pipe(destination)` → `startFlowing(request, destination)` (line 6464) → `flushCompletedQueues` (line 6175); `abort(request, reason)` is the cancellation path (line 6557).
- Server Action trace: HTTP POST FormData → `decodeAction(body, serverManifest)` → detect `$ACTION_REF_*` or `$ACTION_ID_*` → `decodeBoundActionMetaData` (for bound args) → `loadServerReference` → `resolveServerReference(bundlerConfig, id)` → `preloadModule` → `requireModule` → `bindArgs(fn, args)` → returned action `fn.bind(null, formData)`.
- TS compile trace: `compileFn` builds `Environment` from `findContextIdentifiers(func)`, then runs `lower(func, env)` → `PruneMaybeThrows` → `ValidateContextVariableLValues` → `MergeConsecutiveBlocks` → `SSA` → `EliminateRedundantPhi` → `ConstantPropagation` → `InferTypes` → `AnalyseFunctions` → `InferMutationAliasingEffects` → `DeadCodeElimination` → `InferMutationAliasingRanges` → `InferReactivePlaces` → `buildReactiveFunction` → `renameVariables`, logging each step through the `log(value)` callback.
- Rust compile trace mirrors it: `compile_fn` → `react_compiler_lowering::lower` → `validate_context_variable_lvalues` → `validate_use_memo` → `enter_ssa` → `eliminate_redundant_phi` → `infer_types` → `analyse_functions` → `infer_mutation_aliasing_effects` → `infer_mutation_aliasing_ranges` → validations → `infer_reactive_places` → `infer_reactive_scope_variables` → scope alignment passes.
- The compiler is fault-tolerant rather than fail-fast: passes call `env.recordError(diagnostic)`, validation passes are wrapped in `env.tryRecord(() => pass(hir))`, and errors are aggregated at the end via `env.hasErrors()` / `env.aggregateErrors()` — `CompilerError.invariant()` is the only category that always throws.
Testing signals
Jest 30 is the runtime test framework, with 544 test files under `packages/**/__tests__`, and it is deliberately unusable directly — `package.json`'s `testRegex` points at `dont-run-jest-directly.js`, forcing everything through `scripts/jest/jest-cli.js`. That CLI selects one of six configs (source, source-www, source-xplat, source-persistent, build, build-devtools) and injects `__DEV__`, `__EXPERIMENTAL__` and `__VARIANT__` globals; individual tests opt in/out of behaviour with `// @gate <flag>` pragmas resolved by `scripts/jest/TestFlags.js`. The compiler uses a different harness entirely: `yarn snap` over ~1,543 fixture files with `.expect.md` snapshots, plus Playwright e2e for the playground and the flight fixture. CI runs the runtime suite as an 18-config × 5-shard matrix; the Rust port has its own workflow gating on `cargo check`, `cargo build`, two shell test scripts and `yarn snap --rust`.
- Jest is pinned at `^30.4.1` (`jest`, `jest-cli`, `jest-environment-jsdom`, `babel-jest`) with `jest-silent-reporter` and `jest-snapshot-serializer-raw` for output control.
- There are 544 test files under `packages/**/__tests__/**/*.js`, spread across the runtime, DevTools shared, scheduler and shared packages.
- `scripts/jest/jest-cli.js` is the only supported entry: it exposes `--project {default,devtools}`, `--releaseChannel {experimental,stable,www-classic,www-modern,xplat}`, `--env`, `--prod`/`--dev`, `--variant`, `--build`, `--persistent`, `--ci`, `--reactVersion` and `--sourceMaps`, then spawns Jest with the matching config file.
- Six Jest configs cover the test surfaces: `config.source.js`, `config.source-www.js`, `config.source-xplat.js`, `config.source-persistent.js`, `config.build.js` and `config.build-devtools.js` — so the same specs are exercised against source and against built bundles.
- `@gate` pragmas let a test declare its preconditions; `scripts/jest/TestFlags.js` resolves them against `shared/ReactFeatureFlags` plus environment flags `__DEV__`, `build`, `experimental`, `stable`, `variant`, `persistent`, and the deliberate escape hatches `FIXME: false` / `TODO: false` for known-broken tests.
- `scripts/jest/setupHostConfigs.js` rewires module resolution so each test file resolves `react-reconciler/src/ReactFiberConfig` to the right renderer's host config, iterating over `inlinedHostConfigs` — without it every reconciler test would hit the throwing shim.
- `packages/internal-test-utils` supplies the shared test vocabulary — `internalAct.js`, `ReactInternalTestUtils.js`, `consoleMock.js`, `shouldIgnoreConsoleError.js`/`shouldIgnoreConsoleWarn.js`, and `simulateBrowserEventDispatch.js` — and is itself tested.
- The runtime CI matrix in `runtime_build_and_test.yml` runs 18 parameter sets (stable/experimental/www-classic/www-modern/xplat × development/production × variant true/false, plus two persistent configs) crossed with 5 shards, i.e. 90 parallel `yarn test` jobs.
- CI also gates on non-test checks as separate jobs: `flow` (sharded by discovered inline configs), `check_generated_fizz_runtime`, `flags`, and `test-linter` for `eslint-plugin-react-hooks`.
- There is a dedicated fuzz workflow (`runtime_fuzz_tests.yml`) separate from the main suite, so fuzzing is not part of the per-PR signal.
- The compiler's test corpus is fixture-driven: ~1,543 `.js` fixtures under `babel-plugin-react-compiler/src/__tests__/fixtures`, each paired with a `.expect.md`, run via `yarn snap` (`-p <pattern>`, `-d` debug, `-u` update) as documented in `compiler/CLAUDE.md`.
- Fixture naming encodes expected failure modes: `error.todo-*.js` means an unsupported feature that correctly bails with a Todo error, `error.bug-*.js` means a known bug producing the wrong error — so some fixtures are assertions about known gaps, not passing behaviour.
Risk map
The genuinely security-sensitive surface is small but sharp: Flight's Server Actions turn attacker-controlled FormData into a module lookup and function invocation, the taint registry is the only guard against serialising server secrets to the client and it is experimental-only, and the DevTools extension bridges page-world `postMessage` to privileged `chrome.runtime` ports while also running string-`eval` payloads through `chrome.devtools.inspectedWindow.eval`. Operationally the biggest fragility is the fork/shim system — `ReactFiberConfig` and `ReactFeatureFlags` resolve differently under Rollup, Jest and Flow, so a change that passes locally can break a channel nobody ran. `ReactFiberWorkLoop.js` at 5,683 lines with ~60 exported functions and module-level mutable state is the highest blast-radius file in the repo, and the Rust port is a live second implementation of the compiler that must stay snapshot-identical to the TypeScript one.
- `loadServerReference` resolves a client-supplied string id straight into `resolveServerReference(bundlerConfig, id)` → `preloadModule` → `requireModule`; the only validation is `typeof id !== 'string'` (which silently returns `null as any`), so the security boundary rests entirely on the bundler-generated `serverManifest`.
- `decodeAction` distinguishes action fields purely by string prefix (`$ACTION_REF_`, `$ACTION_ID_`, `$ACTION_`) on attacker-controlled FormData keys, and a mismatch falls through to `throw new Error('Cannot handle action key. This is a bug in React.')` — a user-reachable throw framed as an internal invariant.
- The taint protection against leaking secrets to the client is gated off by default outside experimental builds (`export const enableTaint = __EXPERIMENTAL__`), and `taintUniqueValue` early-returns when the flag is false — stable consumers get no taint enforcement.
- `TaintRegistryValues` is a plain `Map<string|bigint, TaintEntry>` with manual refcounting in `cleanup()`, relying on `FinalizationRegistry`; the code explicitly notes that if `FinalizationRegistry` is absent it 'assume[s] that objects live forever' — a correctness cliff on older runtimes.
- The DevTools extension executes code as strings in the inspected page: `evalScripts.js` defines a `.code` variant for each of `checkIfReactPresentInInspectedWindow`, `reload`, `viewElementSource`, `viewAttributeSource` and the selection bridges, intended for `chrome.devtools.inspectedWindow.eval()`.
- The extension's content scripts relay untrusted page-world messages into privileged extension ports: `prepareInjection.js` listens on `window.addEventListener('message', ...)` and `proxy.js` forwards across `chrome.runtime.connect(...)` / `chrome.runtime.onMessage` and back out via `window.postMessage` — a classic content-script trust boundary.
- The DevTools bridge lets the frontend write arbitrary values into a live app — `overrideProps`, `overrideState`, `overrideHookState`, `overrideContext` are first-class bridge events — so anything that can speak the bridge protocol can mutate application state.
- `__REACT_DEVTOOLS_GLOBAL_HOOK__` is installed as a page global and documented in-source as 'a de facto public API', giving any script on the page a handle on every mounted renderer.
- `ReactFiberWorkLoop.js` is 5,683 lines with roughly 60 exports and heavy module-level mutable state (`workInProgressRoot`, `workInProgressSuspendedReason`, `workInProgressRootExitStatus`, `executionContext`), making it the single highest blast-radius file — almost every reconciler behaviour change lands here.
- The fork system is fragile by design: `scripts/rollup/forks.js` substitutes 13 module paths (shared internals, feature flags, fiber config, stream/fizz/flight configs, `EventListener`, `useSyncExternalStore`) and Jest substitutes them differently — so a module can resolve three ways (Rollup, Jest, Flow) and only CI exercises all of them.
- There are 13 `ReactFeatureFlags` forks (`www`, `www-dynamic`, `native-fb`, `native-oss`, `test-renderer`, `eslint-plugin.www`, `readonly`, …); a flag added to the base file without updating every fork is a known failure mode, which is exactly why CI has a dedicated `flags` job and a `/flags` skill.
- `scripts/release/ci-npmrc` writes `//registry.npmjs.org/:_authToken=${NPM_TOKEN}` and three compiler prerelease workflows inject `NPM_TOKEN` from repository secrets — any workflow change that widens trigger conditions on those files widens publish access.
Extension points
The repo's main extension mechanism is the host-config pattern: write a `ReactFiberConfig.<renderer>.js`, register the renderer in `scripts/shared/inlinedHostConfigs.js` and add a fork rule, and you get a new React renderer sharing the reconciler unchanged. Bundles are similarly table-driven — add an entry to `scripts/rollup/bundles.js` and a version to `ReactVersions.js` and the build/publish pipeline picks it up. Inside the compiler, the pass list in `Pipeline.ts` / `pipeline.rs` is the ordered registry, `HIR/Globals.ts` is where hook aliasing signatures are declared, and `Environment.ts` is where feature flags live. For tooling, the ESLint plugin merges rules from a `allRules` map, the MCP servers register tools with `server.tool(name, description, schema, handler)`, and Jest behaviour is extended via `@gate` flags declared in `TestFlags.js`.
- New renderers plug in through the host-config shim: implement the interface, then register in `scripts/shared/inlinedHostConfigs.js` with a `shortName` and `entryPoints`, and add a substitution branch in `scripts/rollup/forks.js` for `./packages/react-reconciler/src/ReactFiberConfig.js`.
- `packages/react-noop-renderer` is the reference implementation of that extension point — a full renderer with `persistent.js`, `server.js`, `flight-client.js`, `flight-server.js` and `flight-modules.js` variants, used as the testbed for reconciler features.
- New publishable packages plug into `ReactVersions.js`: the header states 'Packages will not be published unless they are added here', with `stablePackages` for versioned output and `experimentalPackages` for sha-only builds.
- New build outputs plug into the `bundles.js` table via `bundleTypes` and `moduleTypes` (`ISOMORPHIC`, `RENDERER`, …); `scripts/rollup/build.js#shouldSkipBundle` then decides per bundle×type whether to emit.
- `scripts/rollup/forks.js` is itself an extension registry — a map from module path to a substitution function, currently covering `ReactSharedInternals`, `ReactDOMSharedInternals`, `ReactFeatureFlags`, `SchedulerFeatureFlags`, `DefaultPrepareStackTrace`, `ReactFiberConfig`, `ReactServerStreamConfig`, `ReactFizzConfig`, `ReactFlightServerConfig`, `ReactFlightClientConfig`, `EventListener`, `useSyncExternalStore` and `isServerEnvironment`.
- New runtime behaviour is introduced behind a flag in `packages/shared/ReactFeatureFlags.js`, mirrored into every fork under `packages/shared/forks/`, and surfaced to tests automatically as a `@gate` name by `TestFlags.js` (which requires `shared/ReactFeatureFlags` on demand).
- The reconciler exposes a stable seam for schedulers: `ReactFiberRootScheduler.js#scheduleCallback` wraps `Scheduler_scheduleCallback(priorityLevel, callback)`, and the Scheduler itself is forked into `SchedulerMock`, `SchedulerNative`, `SchedulerPostTask` variants.
- New compiler passes are inserted by editing the ordered pass list in `Entrypoint/Pipeline.ts` (and its Rust twin `entrypoint/pipeline.rs`); `compiler/CLAUDE.md` mandates reading `docs/passes/` for the pass being changed first.
- Hook semantics are extensible through aliasing signatures registered in `src/HIR/Globals.ts` via `addHook(DEFAULT_SHAPES, {positionalParams, restParam, returnType, calleeEffect, hookKind, returnValueKind, aliasing: {receiver, params, rest, returns, temporaries, effects}}, id)` — missing an `aliasing` block silently falls back to `DefaultNonmutatingHook`.
- Compiler feature flags are configured in `src/HIR/Environment.ts` and can be overridden per test fixture by a first-line comment pragma (e.g. `// enableJsxOutlining @enableNameAnonymousFunctions:false`).
- The Rust crates are the extension surface for the port: eleven single-responsibility crates (`react_compiler_lowering`, `_ssa`, `_typeinference`, `_inference`, `_optimization`, `_reactive_scopes`, `_validation`, `_hir`, `_ast`, `_diagnostics`, `_utils`) wired through `Cargo.toml` workspace dependencies.
- The ESLint plugin composes its rule table from two hand-written rules plus a spread of `allRules` from `shared/ReactCompiler`, and derives `recommended`/`recommendedLatest` presets by mapping compiler severities through `mapErrorSeverityToESlint` — new compiler diagnostics become lint rules without touching `index.ts`.
Contributor handoff
Treat `/compiler/` and everything else as two separate projects with separate package managers, lockfiles, CI workflows and conventions — the root `CLAUDE.md` says so explicitly, and `compiler/CLAUDE.md` even documents Sapling (`sl`) rather than git for that subtree. Never run `jest` or `rollup` directly; use `yarn test` / `yarn flow` / `yarn linc` / `yarn prettier`, and let CI do builds. The single most important structural fact to internalise before editing is the fork/shim system: `ReactFiberConfig`, `ReactFeatureFlags` and the flight/fizz configs resolve differently under Rollup, Jest and Flow, so 'it works in one channel' is not a signal. Read the work loop, the root scheduler and the feature-flag file before touching reconciler behaviour, and read the relevant `docs/passes/` doc before touching a compiler pass.
- The repo is explicitly split: root `CLAUDE.md` scopes itself to 'All files outside `/compiler/`' and defers the compiler to its own instructions; `compiler/` has its own `package.json`, `yarn.lock`, `Cargo.toml` and CI workflows.
- Read first, in order: `CLAUDE.md`, `packages/react-dom/src/client/ReactDOMRoot.js` (entry), `packages/react-reconciler/src/ReactFiberWorkLoop.js` (the engine), `packages/react-reconciler/src/ReactFiberRootScheduler.js` (when work runs), `packages/shared/ReactFeatureFlags.js` (what's on), and `scripts/rollup/forks.js` + `scripts/shared/inlinedHostConfigs.js` (how modules actually resolve).
- The prescribed pre-commit gate is `/verify`: `yarn prettier` → `yarn linc` (stop on failure), then `/flow`, `/test` and `/test www` in parallel — mirroring the CI jobs, so passing it locally is a strong predictor of green CI.
- Yarn 1 is mandatory (`packageManager: yarn@1.22.22`) and Node is pinned by `.nvmrc`, which every CI job consumes via `node-version-file: '.nvmrc'`.
- Do not invoke Jest directly — the root `jest.testRegex` deliberately points at `scripts/jest/dont-run-jest-directly.js`; run `yarn test -r <channel> --env <env>` instead, and `yarn test-www` / `yarn test-stable` for the common channels.
- Do not build locally unless told to: root `CLAUDE.md` states 'Builds are handled by CI', and `yarn build` runs a multi-channel Rollup + Closure pipeline that also shells out to git and rewrites versions.
- Adding a feature flag is a multi-file edit: the base `packages/shared/ReactFeatureFlags.js` plus each of the 13 forks under `packages/shared/forks/`, verified by `yarn flags` and the CI `flags` job.
- Adding a user-visible `throw` requires running `yarn extract-errors` and committing the new entry in `scripts/error-codes/codes.json`; the script greps the `build` directory, so it only works after a build exists.
- Tests that depend on a flag or channel must carry a `// @gate <expr>` pragma (supports `&&`, `!`, multiple gates); `FIXME`/`TODO` are reserved gate names that disable a test wholesale.
- Compiler work has its own loop: `yarn snap` to run fixtures, `yarn snap -p <pattern> -d` to dump per-pass HIR, `yarn snap -u` to update `.expect.md`, `yarn snap compile <path>` for arbitrary files, `yarn snap minimize <path>` to shrink a repro.
- Before implementing from a Rust-port plan doc, `compiler/CLAUDE.md` requires checking `git log --oneline --grep="<plan-name>"` and the plan's Remaining Work section; afterwards update the plan status, run `/compiler-verify`, and ensure `compiler/scripts/test-babel-ast.sh` passes.
- Trap: `packages/react-reconciler/src/ReactFiberConfig.js` throws on import by design. If a new test or script resolves it, the fix is registering the renderer in `scripts/jest/setupHostConfigs.js` / `inlinedHostConfigs.js`, not editing the shim.
Languages
| Language | Files | Share of source |
|---|---|---|
| JavaScript | 3915 | 85% |
| Markdown | 2011 | docs/config |
| TypeScript | 543 | 12% |
| JSON | 153 | docs/config |
| Rust | 121 | 3% |
| CSS | 108 | docs/config |
| HTML | 79 | docs/config |
| YAML | 31 | docs/config |
Start reading here
Probable source entrypoint: packages/ — inferred from the directory tree and manifest, not the upstream docs.
Source roots
packages/ (1610 files)
Repository structure
| Top-level path | Files |
|---|---|
compiler/ | 1611 |
packages/ | 1610 |
fixtures/ | 505 |
scripts/ | 195 |
.github/ | 32 |
flow-typed/ | 11 |
.claude/ | 9 |
.codesandbox/ | 1 |
Docs found: CLAUDE.mdCONTRIBUTING.mdREADME.md
Package
package.json — , 80 declared dependencies.
@babel/cli@babel/code-frame@babel/core@babel/helper-define-map@babel/helper-module-imports@babel/parser@babel/plugin-external-helpers@babel/plugin-proposal-class-properties@babel/plugin-proposal-object-rest-spread@babel/plugin-syntax-dynamic-import@babel/plugin-syntax-import-meta@babel/plugin-syntax-jsx
CI & config entrypoints
README.md— Repository readmeCLAUDE.md— Claude instructions.github/workflows/compiler_discord_notify.yml— GitHub Actions workflow.github/workflows/compiler_playground.yml— GitHub Actions workflow.github/workflows/compiler_prereleases.yml— GitHub Actions workflow.github/workflows/compiler_prereleases_manual.yml— GitHub Actions workflow.github/workflows/compiler_prereleases_nightly.yml— GitHub Actions workflow.github/workflows/compiler_rust.yml— GitHub Actions workflow.github/workflows/compiler_rust_publish.yml— GitHub Actions workflow.github/workflows/compiler_rust_version.yml— GitHub Actions workflow
Structural health signals
Average score 5.4/10 across 96 analyzed source files; 92 hotspots surfaced.
packages/react-dom/src/__tests__/ReactDOMFloat-test.js— 1536 lines, score 5 (Large file, Deep nesting)compiler/crates/react_compiler_lowering/src/build_hir.rs— 1785 lines, score 5 (Large file, Deep nesting)packages/react-reconciler/src/ReactFiberCommitWork.js— 1463 lines, score 5 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
59aff3e18cb52026-09-18 — [compiler] Bail on use of arguments object (#37645)71f7255937392026-09-17 — [Flight] Server References for arbitrary object types (#37636)2b19aecd0e912026-09-16 — [rust-compiler] Fix exponentiation to match js semantics (#37587)98cec2ec262e2026-09-16 — [compiler] Add regression test for useEffectEvent reading a variable declared later (#37618)2c68870bcc452026-09-16 — [test] Stop polyfilling `AbortController` (#37633)