Architecture Overview and Navigating the React Source
Prerequisites
- ›Basic JavaScript and module systems (import/export)
- ›Familiarity with React as a user (components, JSX, hooks)
- ›General understanding of build tools (bundlers, module resolution)
Architecture Overview and Navigating the React Source
React's source code is intimidating — not because any single file is unreasonably complex, but because the codebase is an ecosystem of ~38 packages wired together by a custom build pipeline that performs compile-time module replacement. Before you can understand how reconciliation works or how hooks store state, you need a mental map of where things live and how they connect. This article gives you that map.
We'll walk through the monorepo layout, trace the dependency graph between packages, and then deep-dive into React's most distinctive architectural pattern: the fork system, which swaps out entire modules at build time to produce different bundles for open source, Meta's web properties (FB_WWW), and React Native.
Monorepo Layout and Package Roles
React is a Yarn workspaces monorepo. The root package.json declares a single workspace glob:
{
"private": true,
"workspaces": ["packages/*"]
}
All publishable packages live under packages/. Version numbers are centralized in ReactVersions.js, which defines the single source of truth — currently 19.3.0 — and maps each package name to its version. This prevents version drift across the monorepo.
| Directory | Role |
|---|---|
packages/react |
Public API surface — createElement, hooks stubs, component types |
packages/react-reconciler |
The Fiber reconciler — shared core used by all renderers |
packages/react-dom |
DOM renderer entry points (createRoot, hydrateRoot) |
packages/react-dom-bindings |
DOM-specific host config, event system, prop handling |
packages/react-server |
Server rendering engines — Fizz (SSR) and Flight (RSC) |
packages/react-client |
Flight client — deserializes RSC payloads |
packages/scheduler |
Cooperative scheduling with priority queues |
packages/shared |
Cross-cutting utilities, feature flags, shared types |
packages/react-native-renderer |
React Native host config and renderer |
compiler/ |
React Compiler (formerly React Forget) — separate build system |
The compiler/ directory is a completely separate project with its own package.json and build pipeline. It's not part of the Yarn workspaces.
The Package Dependency Graph
React's package architecture follows a strict layering that enables renderer-agnosticism. Understanding this graph is essential for navigating the source.
graph TD
react["react<br/>(Public API)"]
shared["shared<br/>(Utilities, Types, Feature Flags)"]
reconciler["react-reconciler<br/>(Fiber Engine)"]
dom["react-dom<br/>(DOM Renderer)"]
domBindings["react-dom-bindings<br/>(DOM Host Config + Events)"]
native["react-native-renderer<br/>(Native Renderer)"]
scheduler["scheduler<br/>(Priority Queue)"]
dom --> reconciler
dom --> domBindings
dom --> react
native --> reconciler
native --> react
reconciler --> shared
reconciler --> scheduler
react --> shared
domBindings --> shared
The critical insight: the react package has no dependency on any renderer. It exports hook stubs like useState that delegate to whatever dispatcher is currently installed — but it doesn't know or care whether that dispatcher comes from react-dom or react-native-renderer.
The ReactClient.js file imports hooks from ./ReactHooks, which reads the current dispatcher from ReactSharedInternals.H. The actual hook implementations live in the reconciler. This indirection is the foundation of React's multi-renderer architecture.
The ReactSharedInternals.js bridge module in shared/ simply imports from react and re-exports:
import * as React from 'react';
const ReactSharedInternals =
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
export default ReactSharedInternals;
But this creates a circular dependency when building the react package itself — react can't import from react. This is where the fork system comes in.
The Fork System — Compile-Time Dependency Injection
React's most unique architectural pattern is the fork system defined in scripts/rollup/forks.js. It replaces module paths at build time based on the bundle type and entry point.
The forks object maps source file paths to functions that return replacement paths:
flowchart LR
A["Import: shared/ReactSharedInternals"] --> B{Which bundle?}
B -->|"entry = 'react'"| C["react/src/ReactSharedInternalsClient.js"]
B -->|"entry = 'react/src/ReactServer.js'"| D["react/src/ReactSharedInternalsServer.js"]
B -->|"condition = 'react-server'"| E["react-server/src/ReactSharedInternalsServer.js"]
B -->|"Other packages"| F["shared/ReactSharedInternals.js (no replacement)"]
There are three major forks:
1. ReactSharedInternals — Solves the circular dependency. When building the react package, importing shared/ReactSharedInternals is replaced with the direct ReactSharedInternalsClient.js source, which defines the shared state object with its terse property names (H for hooks dispatcher, A for async dispatcher, T for transition, S for startTransition callback, G for gesture).
2. ReactFeatureFlags — Per-environment flag values. The master flag file at packages/shared/ReactFeatureFlags.js defines defaults, but each bundle type gets its own fork. The forks.js logic for feature flags dispatches based on entry and bundle type.
3. ReactFiberConfig — The host config injection. The source file ReactFiberConfig.js is a throw sentinel:
throw new Error('This module must be shimmed by a specific renderer.');
This file must be replaced at build time. The fork system looks up the renderer's inlinedHostConfigs entry and finds the corresponding fork file — for DOM, that's ReactFiberConfig.dom.js, which re-exports from react-dom-bindings.
Tip: If you're reading reconciler code and see imports from
./ReactFiberConfig, remember that these are abstract operations — they don't resolve to the throw sentinel at runtime. The build system replaces them with renderer-specific implementations. This is React's version of dependency injection.
Feature Flags as Architecture
React ships to four distinct environments simultaneously: open source (npm), Meta's web properties (FB_WWW), React Native at Meta (RN_FB), and React Native open source (RN_OSS). Feature flags enable gradual rollout across all of them.
The master feature flags file organizes flags by lifecycle stage with clear section comments:
| Lifecycle Stage | Meaning | Example |
|---|---|---|
| Land or remove (zero effort) | Ready to clean up | (currently empty) |
| Killswitch | Just shipped; can turn off if regression | Flags like disableSchedulerTimeoutInWorkLoop |
| Ongoing experiments | Actively being developed | enableYieldingBeforePassive, enableGestureTransition |
| Ready for next major | Ships in the next breaking version | Various __NEXT_MAJOR__ flags |
The ReactFeatureFlags.www.js fork for Meta's web properties shows how flags can be dynamically loaded at runtime via require('ReactFeatureFlags'), while still having static overrides for certain values. This hybrid approach lets Meta's infrastructure do per-employee or percentage-based rollouts.
At build time, feature flags compile down to true or false constants. The bundler's dead code elimination then strips unreachable branches, so disabled features add zero bytes to the production bundle.
Build Pipeline and Bundle Types
The build pipeline is orchestrated by scripts/rollup/bundles.js, which defines two key enumerations:
Module Types classify what kind of package is being built:
flowchart TD
ISO["ISOMORPHIC<br/>react, react-jsx-runtime<br/>Works everywhere"]
REND["RENDERER<br/>react-dom, react-native-renderer<br/>Bundles the reconciler"]
RECON["RECONCILER<br/>react-reconciler (standalone npm)<br/>For custom renderers"]
RUTIL["RENDERER_UTILS<br/>Helpers accessing renderer internals"]
Bundle Types define the target environment and mode:
| Bundle Type | Target | Example |
|---|---|---|
NODE_DEV / NODE_PROD |
Open source npm, dev/prod | react-dom.development.js |
FB_WWW_DEV / FB_WWW_PROD |
Meta internal web | Custom build with dynamic flags |
RN_FB_DEV / RN_FB_PROD |
React Native at Meta | Internal mobile builds |
RN_OSS_DEV / RN_OSS_PROD |
React Native open source | Published to npm |
ESM_DEV / ESM_PROD |
ES module builds | For modern bundlers |
The inlinedHostConfigs.js file maps renderer short names to their entry points and the paths they're allowed to import. The dom-browser config, for example, lists entry points like react-dom, react-dom/client, and react-server-dom-webpack/src/server/react-flight-dom-server.browser, along with all allowed import paths.
flowchart LR
Source["Source Files<br/>(Flow-typed JS)"] --> Rollup["Rollup Build"]
Rollup --> Forks["Fork System<br/>(Module Replacement)"]
Forks --> Flags["Feature Flag Inlining<br/>(Dead Code Elimination)"]
Flags --> Bundles["Output Bundles"]
Bundles --> NPM["npm packages<br/>(NODE_DEV/PROD)"]
Bundles --> FB["Meta internal<br/>(FB_WWW)"]
Bundles --> RN["React Native<br/>(RN_FB/RN_OSS)"]
The complete path from source to published package involves: Rollup reads the entry point → the fork system replaces modules based on (bundleType, entry) pairs → Babel transpiles Flow types → feature flags are inlined → Closure Compiler (for some bundles) or Terser minifies → output lands in build/ directory.
Tip: When exploring the codebase, always check
scripts/rollup/forks.jsif an import seems to resolve to a "wrong" file. The actual module your renderer uses may be entirely different from what's on disk at the import path.
What's Next
With this mental map in hand, you know where React's packages live, how they relate, and how the build pipeline transforms them for different targets. In the next article, we'll zoom into the most fundamental data structure in the entire codebase: the Fiber node — the mutable JavaScript object that represents every component, DOM element, and boundary in your React tree.