Read OSS

Architecture Overview and Navigating the React Source

Intermediate

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.js if 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.