React — The Reconciler at the Center

ContextDx logoContextDx
Build something like this?
Insights5

Architecture dossier

React — The Reconciler at the Center

The React monorepo around its center of gravity: a shared Fiber reconciler (paced by the scheduler, built on the shared kernel) driven by the core API and fanned out to six renderers and a Flight/Fizz server stack. A few central nodes carry everything — see insights for the render-path, blast-radius, and coupling views.

21 findings · 10 traced paths across this system.

Reliability & Blast Radius — 2026-06-05

3 insights · 2 paths

Reliability & Blast Radius Report

Summary

3 findings: 1 critical, 2 high, 0 medium, 0 low Polarities: 3 risks, 0 strengths, 0 opportunities, 0 observations 2 paths traced, 1 suggestion

Findings

1. Fiber Reconciler Is the Critical SPOF (risk · critical · confidence: verified)

Element: react-reconciler File: packages/react-reconciler

Seven inbound dependents (six renderers + DevTools + test-infra) embed the one reconciliation engine — no redundancy.

Measurement: 7 inbound dependents Recommendation: Treat the host-config shape as a versioned contract; gate reconciler changes behind cross-renderer contract tests.


2. Shared Kernel Is the Hidden Ring-0 Root (risk · high · confidence: verified)

Element: shared File: packages/shared

~125 import sites in the reconciler alone, plus react-dom-bindings and server-components. One ring deeper than the reconciler.

Measurement: 125 import sites (trend stable) Recommendation: Freeze shared's public surface behind a typed facade; lint against deep imports.


3. DOM Renderer Concentrates Client + SSR Risk (risk · high · confidence: likely)

Element: react-dom File: packages/react-dom

The only renderer also depending on server-components (Fizz) and react-dom-bindings — its failure removes both CSR and SSR for the web.

Recommendation: Keep client and Fizz server entrypoints on separate release gates.

Paths

Reconciler Failure Cascade (risk · critical) shared → react-reconciler (Ring 1 fan-out: react-art, react-native-renderer, react-test-renderer, react-noop-renderer, devtools) → react-dom → server-components

Scheduler Outage Stalls All Reconciliation (risk · high) scheduler → react-reconciler

Suggestions

  • add container — Introduce a versioned host-config contract layer to localize breaking reconciler changes.
Trace a path on the diagram

Reconciler Failure Cascade

How a fault in the shared kernel and Fiber reconciler fans out across the entire renderer layer.

  1. 1.Ring 0: kernel flags + React symbols
  2. 2.Ring 0: Fiber engine embedded by all renderers
  3. 3.Ring 1: browser rendering fails hard
  4. 4.Ring 2: SSR/streaming degrades (Fizz via react-dom)

Scheduler Outage Stalls All Reconciliation

The upstream direction: the reconciler depends on the scheduler, so a scheduler stall freezes reconciliation itself.

  1. 1.Ring 0: cooperative scheduler stalls
  2. 2.Cannot yield/resume — all renders freeze

risk3

Fiber Reconciler Is the Critical SPOF

riskcriticalverifiedlarge

Six host renderers plus DevTools and test-infra embed react-reconciler — the highest fan-in on the board (7 inbound dependents). It has no redundancy: there is exactly one reconciliation engine.

Impact: A breaking change or regression in the reconciler propagates to every renderer and DevTools simultaneously — Ring 1 is the entire renderer layer.

Treat the host-config shape as a versioned contract and gate reconciler changes behind cross-renderer contract tests before each release.

spofreconciliation

Shared Kernel Is the Hidden Ring-0 Root

riskhighverifiedmedium

react-reconciler imports ~125 symbols from shared (feature flags, React symbols/types); react-dom-bindings and server-components also depend on it. shared sits one ring deeper than the reconciler itself.

Impact: A change in shared's flags or types silently alters the reconciler, DOM bindings, and server stack at once.

Freeze shared's public surface behind a typed facade and lint against deep imports so kernel changes are explicit and reviewable.

spofkernel

DOM Renderer Concentrates Client + SSR Risk

riskhighlikelymedium

react-dom is the only renderer that also depends on server-components (Fizz SSR) and react-dom-bindings, so its failure removes both client-side and server-side rendering for the web simultaneously.

Impact: Web apps lose both CSR and SSR paths if react-dom regresses.

Keep the client and Fizz server entrypoints on separate release gates so a regression in one cannot ship the other.

spofssr

Feature-Driven Journey — 2026-06-05

3 insights · 2 paths

Feature-Driven Journey Report

Summary

3 findings: 0 critical, 2 high, 1 medium, 0 low Polarities: 0 risks, 0 strengths, 0 opportunities, 3 observations 2 paths traced, 1 suggestion

Findings

1. DOM Renderer Is the Browser Entrypoint (observation · high · confidence: verified)

Element: react-dom File: packages/react-dom

createRoot().render() is the single client entrypoint that drives the Fiber reconciler with a DOM host config. It peer-depends on react for the element contract and delegates events to react-dom-bindings.

Context: Every browser render enters here; the same package also exposes the Fizz server entrypoints.


2. Reconciler Builds the Fiber Tree (observation · high · confidence: verified)

Element: react-reconciler File: packages/react-reconciler

Diffing, lane prioritization, hook state and effects, then commit prep. Renderer-agnostic — embedded by every host renderer via a build-time host config.


3. Scheduler Time-Slices the Work (observation · medium · confidence: verified)

Element: scheduler File: packages/scheduler

The reconciler yields here between units of work for interruptible rendering. This is the latency-sensitive hop — long tasks delay paint.

Paths

Browser Client Render Pipeline (observation · high) react → react-dom (branch: react-dom-bindings) → react-reconciler → scheduler

Test-Renderer Journey (observation · medium) react-test-renderer (branch: react-is) → react-reconciler → scheduler

Suggestions

  • add node — Add a dev-only render-tracing overlay: the render path has no single observability surface for per-hop latency.
Trace a path on the diagram

Browser Client Render Pipeline

The complete forward path a browser render takes, from the public API to scheduled commit work.

  1. 1.Public API: createElement / JSX / hooksRenderer-agnostic element contract
  2. 2.createRoot().render() — host entrypoint
  3. 3.Diff & build the Fiber tree
  4. 4.Time-slice & yield between units of work

Test-Renderer Journey

How the same reconciliation engine is exercised without a DOM, for snapshot testing.

  1. 1.Render React tree to plain JS objects
  2. 2.Reconcile with an in-memory host config
  3. 3.Same scheduling primitives as production

observation3

DOM Renderer Is the Browser Entrypoint

observationhighverified

react-dom's createRoot().render() is the single client entrypoint that drives the Fiber reconciler with a DOM host config (packages/react-dom). It peer-depends on react for the element contract and delegates events to react-dom-bindings.

Every browser render enters the system here before any reconciliation happens; the same package also exposes the Fizz-based server entrypoints.

renderentrypoint

Reconciler Builds the Fiber Tree

observationhighverified

react-reconciler performs diffing, lane prioritization, hook state and effects, then prepares commit work (packages/react-reconciler). It is renderer-agnostic and embedded by every host renderer via a build-time host config.

This is the structural heart of the journey: the same engine runs for DOM, Native, ART, and test renderers.

reconciliation

Scheduler Time-Slices the Work

observationmediumverified

react-reconciler yields to the scheduler between units of work, enabling time-slicing and interruptible rendering (packages/scheduler) via MessageChannel/microtask yielding.

This is the latency-sensitive hop in the journey: long tasks here delay paint and block input responsiveness.

schedulinglatency

Future-Readiness — What would have to break for external state reads — 2026-06-02

5 insights · 1 path

Future-Readiness — What would have to break for an external state-read primitive to exist

Framing

The absence is not an oversight; it is a load-bearing boundary. Three invariants would each have to break — and they are not equally reversible.

Seam 1 — the render-only dispatcher gate (large, additive, partially reversible)

Every read funnels through ReactSharedInternals.H, live only between renderWithHooks and finishRenderingHooks (ReactFiberHooks.js:511,656,664). A read-from-outside API needs a render-independent read path. First safe step: never expose raw fiber reads — expose committed-tree snapshots built on the existing useSyncExternalStore consistency machinery, never in-flight WIP state.

Seam 2 — concurrency makes 'the state' multi-valued (IRREVERSIBLE)

Double buffering (fiber.alternateReactInternalTypes.js:174; createWorkInProgressReactFiber.js:327-355) plus the scheduler's interruptible lanes mean committed and in-flight values are simultaneously live. Any external read must choose committed-vs-in-flight semantics or tear. This is the load-bearing wall — it is the same property that makes time-slicing, Suspense, transitions, and Offscreen safe. Exposing uncontrolled reads would forfeit React's ownership of when reads happen, and could not be walked back.

Seam 3 — positional hook identity (epic, breaking, but local)

Hooks are addressed by call order, not by key (ReactFiberHooks.js:194-200,263-266). There is no name to read by. Keyed identity would mean changing the hook model itself. Don't retrofit hooks — the keyed, addressable path already exists as useSyncExternalStore.

The safe seam already exists and points the other way

useSyncExternalStore (:1633) is React's expand-contract bridge: snapshot per render + checkIfSnapshotChanged forcing a sync re-render on tearing (:1846-1850). react-cache was an earlier experiment in the same spirit. The reversible evolution is to keep state outside the tree and enrich the doorway (selectors, derived snapshots, transition-aware reads) — not to drill a hole into fiber internals.

Ranked roadmap (by reversibility)

SeamEffortReversibilityVerdict
Hook identity (keyed)EpicIrreversible (local API)Avoid — use external stores
Dispatcher gate (2nd read path)LargePartially reversible (additive)Only as committed-snapshot reads
Concurrency read-ownershipIrreversible (global)Do not break — it is the foundation

Suggestion

Add an explicit dispatcher-bridge edge react ⇒ react-reconciler to the board. The two packages have no direct import edge — they are joined only by the mutable ReactSharedInternals.H slot during render. Making that runtime bridge visible explains at a glance why reads are render-gated and why an external primitive has nowhere to attach.

Trace a path on the diagram

What an external-read primitive would have to cross

The three structural barriers, in increasing order of irreversibility, that any 'read state from outside' API would have to break through.

  1. 1.Where such a primitive would live — the public surface
  2. 2.Must bypass the render-only dispatcher gate (seam 1, large/additive)
  3. 3.Must define semantics against concurrent lanes — the irreversible wall

risk1

Seam 2 (irreversible) — concurrency makes 'the state' multi-valued

riskhighverified

Double buffering (fiber.alternate, ReactInternalTypes.js:174; createWorkInProgress, ReactFiber.js:327-355) plus the scheduler's interruptible lanes mean a committed value and one or more in-flight values are simultaneously live. Any external-read primitive must pick committed-vs-in-flight semantics or accept tearing.

Impact: This is entangled with time-slicing, Suspense, transitions, and Offscreen. Exposing uncontrolled reads would forfeit the guarantee that React decides when reads happen — the precondition for all concurrent features.

This is the irreversible decision. The reason there is no out-bound read is the same reason concurrent rendering is safe: React owns read timing. A primitive that read in-flight state could not be walked back without breaking concurrency guarantees that the whole ecosystem now depends on.

concurrencytearingirreversiblelanes

opportunity3

The safe seam already exists and points the other way

opportunityhighverifiedmedium

useSyncExternalStore (ReactFiberHooks.js:1633) is React's expand-contract bridge for external state: snapshot read each render + checkIfSnapshotChanged forcing a sync re-render on tearing (1846-1850). react-cache was an earlier external-held experiment in the same spirit. The evolution path is not 'expose fiber state' but 'keep state outside and make the doorway richer.'

Impact: Investing here delivers external readability with zero risk to concurrency, because React still controls every read.

Treat useSyncExternalStore as the canonical external-state boundary. Future readability features (selectors, derived snapshots, transition-aware reads) belong on this seam — it is reversible (expand-contract) where a fiber-read API is not.

use-sync-external-storeexpand-contractsafe-seam

Seam 1 — the render-only dispatcher gate would have to gain a second read path

opportunitymediumverifiedlarge

Today every read funnels through ReactSharedInternals.H, set only inside renderWithHooks and nulled at finishRenderingHooks (ReactFiberHooks.js:511,656,664). For an external read primitive to exist, the fiber would need a render-independent read path that does not depend on the dispatcher slot being live.

Impact: A render-independent read path is the minimum architectural surface required; without it 'read state outside render' is structurally a thrown error.

First safe step: do NOT add a raw fiber-read API. Instead expose committed-tree snapshot reads built on the existing useSyncExternalStore consistency machinery, and never surface in-flight work-in-progress state. This keeps React the owner of when reads happen.

seamdispatcherevolution

Seam 3 — positional hook identity would have to become addressable

opportunitymediumverifiedepic

Hooks are addressed by call order, not by key (ReactFiberHooks.js:194-200, 263-266). There is no name to read state by. An external primitive needs stable, keyed identity — which the positional linked-list model cannot provide without changing the hook model itself.

Impact: Retrofitting keys onto positional hooks would be an epic, breaking change to the most-used API in the library.

Don't retrofit hooks. The addressable-state path already exists: useSyncExternalStore gives external stores their own keyed identity. Formalize and enrich the external-store boundary rather than making fiber internals addressable.

seamhooksidentitykeyed-state

observation1

What would have to break — the three invariants

observationhighinferred

An external state-read primitive requires breaking three invariants at once: (1) the render-only dispatcher gate, (2) positional hook identity, and (3) React's ownership of read timing under concurrency. The first two are hard; the third is the one that cannot be unwound without forfeiting time-slicing safety.

Ranked by reversibility: identity (irreversible API change, but local), gate (large, additive), concurrency ownership (the load-bearing wall). Because (3) is exactly what makes Suspense/transitions/Offscreen possible, the rational evolution is to keep state outside the tree and enrich useSyncExternalStore — which is precisely what React has done.

summaryinvariantsroadmap

Hidden Dependency Detection — 2026-06-05

3 insights · 3 paths

Hidden Dependency Detection Report

Summary

3 findings: 0 critical, 2 high, 1 medium, 0 low Polarities: 2 risks, 0 strengths, 0 opportunities, 1 observation 3 paths traced, 1 suggestion

Findings

1. The Tiny Package on Everyone's Hot Path (risk · high · confidence: verified)

Element: scheduler File: packages/scheduler

4 direct dependents (react-dom, react-art, react-native-renderer) plus every reconciliation cycle — high fan-in for a single-purpose utility.

Measurement: 4 direct dependents Recommendation: Pin scheduler to one internal version; add cross-renderer scheduling regression tests.


2. Unpublished Kernel With ~125 Import Sites (risk · high · confidence: verified)

Element: shared File: packages/shared

published:false yet ~125 import sites in the reconciler alone, plus react-dom-bindings and server-components.

Measurement: 125 import sites (trend stable) Recommendation: Treat shared as a versioned internal contract; lint against deep imports.


3. React Is the Universal Peer Hub (observation · medium · confidence: verified)

Element: react File: packages/react

Seven packages peer-depend on react. Expected for the core API, but the most change-sensitive surface on the board.

Measurement: 7 inbound dependents

Paths

Everything Schedules Through One Package (risk · high) react-dom → scheduler (branches: react-art, react-native-renderer, react-reconciler)

The Unpublished Kernel Under the Renderers (risk · high) react-dom-bindings → shared (branches: react-reconciler, server-components)

React: the Universal Peer-Dependency Hub (observation · medium) use-subscription → use-sync-external-store → react (branches: react-cache, react-refresh, devtools, server-components, build-tooling)

Suggestions

  • modify node — Promote shared to a documented internal contract to make the hidden coupling explicit.
Trace a path on the diagram

Everything Schedules Through One Package

Multiple renderers independently declare the scheduler dependency that all reconciliation work funnels through.

  1. 1.One of 4 independent scheduler consumers
  2. 2.Single cooperative scheduler — the chokepoint

The Unpublished Kernel Under the Renderers

shared is reached from the DOM bindings, the reconciler, and the server stack alike.

  1. 1.Imports shared types & feature flags
  2. 2.Unpublished kernel under the renderers

React: the Universal Peer-Dependency Hub

A long thin chain (use-subscription -> use-sync-external-store -> react) terminates at the hub that seven packages depend on.

  1. 1.Legacy subscription hook
  2. 2.Thin shim over useSyncExternalStore
  3. 3.Universal peer dependency — 7 packages point here

risk2

The Tiny Package on Everyone's Hot Path

riskhighverifiedmedium

scheduler is declared as a standalone dependency by react-dom, react-art, and react-native-renderer, and every reconciliation cycle yields to it — 4 direct dependents for a single-purpose utility package whose archetype suggests low coupling.

Impact: A change to time-slicing or yielding behavior silently alters scheduling for every renderer at once.

Pin scheduler to one internal version across renderers and add cross-renderer scheduling regression tests.

couplingscheduling

Unpublished Kernel With ~125 Import Sites

riskhighverifiedmedium

shared (published:false) is imported ~125 times by react-reconciler alone, plus react-dom-bindings and server-components — an internal helper module that structurally underpins the whole system without an external contract.

Impact: Interface or feature-flag changes in shared cascade to the reconciler, DOM bindings, and server stack without any versioned boundary.

Treat shared as a versioned internal contract; lint against deep/unstable imports and document its public surface.

couplingkernel

observation1

React Is the Universal Peer Hub

observationmediumverified

Seven packages — react-dom, server-components, devtools, use-sync-external-store, react-cache, react-refresh, and build-tooling — point at react for the element/runtime contract.

This fan-in is expected for the core API, but it makes the public element contract the single most change-sensitive surface on the board — every consumer must track its semantics.

couplingapi

How Does It Work? — Why no primitive reads state outside the render tree — 2026-06-02

7 insights · 2 paths

How Does It Work? — Why React has no primitive to read state from outside the render tree

The answer in one sentence

React never exposed an external state-read primitive because hook state has no identity independent of a fiber instance, reads are gated to the render pass by a single mutable dispatcher slot, and — most decisively — under concurrent rendering there is no single 'current value' to read: the committed tree and one or more in-flight work-in-progress trees coexist, so any uncontrolled external read would tear.

The four structural facts (all verified in source)

1. State has no identity independent of the instance

Hook state is a positional linked list on fiber.memoizedState (ReactFiberHooks.js:194-200, 263-266). A hook is located by call order within a render of one specific fiber — there is no key, name, or handle. The Rules of Hooks are load-bearing: stable call order is the only thing that makes a value locatable.

2. Reads are gated to the render pass

Every hook calls resolveDispatcher() -> reads ReactSharedInternals.H (ReactHooks.js:24-42). That slot is set inside renderWithHooks (ReactFiberHooks.js:511,548-563) and reset to ContextOnlyDispatcher at finishRenderingHooks (:656,664). Outside render, every state hook resolves to throwInvalidHookError (:442-451, 3870-3896). The public react package and the react-reconciler engine share no direct edge — they communicate only through this one mutable slot. That indirection is the gate.

3. Double buffering = no single value to read

Each fiber is double-buffered via fiber.alternate (ReactInternalTypes.js:174); createWorkInProgress (ReactFiber.js:327-355) keeps a committed current tree and an in-flight work-in-progress tree alive at once. With the scheduler's concurrent lanes a render can be sliced, paused, and resumed — so multiple versions of the same state are live mid-render. 'Read this state' is ambiguous (committed or in-flight?), and reading the wrong one is the definition of tearing.

4. State is born at mount, destroyed at unmount

detachFiberAfterEffects clears fiber.memoizedState = null on unmount (ReactFiberCommitWork.js:1348). No instance -> no state. There is no lifetime window in which a component's state exists as an addressable thing outside an active fiber.

What this makes structurally impossible

  • A stable address for a component's state (no identity beyond the current/WIP fiber pair).
  • Reading state outside a render (the dispatcher slot is null/ContextOnly -> it throws).
  • A single authoritative value to hand out (double buffering + lanes = multiple in-flight versions).

The exception that proves the rule

DevTools is the only place React reads hook state from outside — and it does so by simulating a render: react-debug-tools installs an instrumented dispatcher and re-invokes the component to replay hook calls in order. Even React's own tooling has no static read path.

Why every state library is 'partly a workaround'

useSyncExternalStore is React conceding exactly this: external state must be held outside the fiber and read back in under React's control, with active tearing detection (checkIfSnapshotChanged forces a sync re-render — ReactFiberHooks.js:1846-1850). The shim's own disclaimer says it 'breaks many of the rules of React, and only works because... updates are always synchronous.' Redux/Zustand/Jotai/Recoil/Valtio/MobX aren't patching a missing feature — they occupy the one location where addressable, externally-readable state can safely exist, and use the one sanctioned (inbound-only) door to bring it back in.

Paths traced

  1. Why an external state read throwsreact (resolveDispatcher) -> react-reconciler (render-only slot) -> scheduler (no single value under lanes).
  2. The sanctioned doorwayuse-sync-external-store (truth held outside) -> react (the blessed door) -> react-reconciler (snapshot + tearing check).
Trace a path on the diagram

Why an external state read throws

The chain that turns 'read this component's state from outside render' into either an Invalid-hook-call throw or a torn value.

  1. 1.Public hook calls resolveDispatcher -> reads ReactSharedInternals.H
  2. 2.Slot is set only during renderWithHooks; outside it ContextOnlyDispatcher throws
  3. 3.Even mid-render, concurrent lanes mean no single value exists to read

The sanctioned doorway the workarounds formalize

How external state legitimately enters React — the inbound-only bridge every state library relies on.

  1. 1.Store holds the source of truth outside the fiber; exposes subscribe + getSnapshot
  2. 2.useSyncExternalStore: the one blessed door back in
  3. 3.Snapshot read each render; checkIfSnapshotChanged forces a sync re-render on tearing

strength1

useSyncExternalStore is the sanctioned doorway — and the explicit admission

strengthhighverified

useSyncExternalStore reads getSnapshot() on every render and runs checkIfSnapshotChanged (ReactFiberHooks.js:1633,1876), forcing a synchronous re-render when an interleaved mutation is detected (1846-1850). The shim disclaimer states it only works 'because updates are always synchronous, because concurrent rendering is only available in versions of React that also have a built-in useSyncExternalStore API.'

This hook is React conceding the exact thing the question asks about: external state must be HELD outside the fiber and read back IN under React's control, with active tearing detection. It is the in-bound door, deliberately not an out-bound one — React reads the store, the store never reads the fiber.

use-sync-external-storetearingdoorway

observation6

Double buffering means there is no single 'current value' to read

observationcriticalverified

Each fiber is double-buffered via fiber.alternate (ReactInternalTypes.js:174); createWorkInProgress (ReactFiber.js:327-355) keeps a committed 'current' tree and an in-flight 'work-in-progress' tree alive at once. Under concurrent lanes, multiple in-flight versions of the same state coexist mid-render.

This is the crux. 'Read this component's state' is ambiguous — committed or in-flight? An uncontrolled external read during a sliced render would observe a half-updated tree, i.e. tearing. The coupling of state to the instance exists precisely so React owns *when* every read happens.

double-bufferingconcurrencytearinglanes

State has no identity independent of a fiber instance

observationhighverified

Hook state is a positional linked list hung off fiber.memoizedState (ReactFiberHooks.js:194-200, 263-266). A hook is addressed only by its call order within a render of one specific fiber — there is no name, handle, or stable address you could hold to read it from elsewhere.

Because identity is positional, not keyed, there is nothing for an external reader to target. This is why the Rules of Hooks (stable call order) are load-bearing rather than stylistic — they are the only thing that makes a hook's state locatable at all.

statefiberhooksidentity

Reads are gated to the render pass by the dispatcher slot

observationhighverified

Every hook routes through resolveDispatcher(), which reads ReactSharedInternals.H (ReactHooks.js:24-42). That slot is set only inside renderWithHooks (ReactFiberHooks.js:511,548-563) and reset to ContextOnlyDispatcher when render finishes (656,664). Outside a render the slot is null/ContextOnly, so every state hook hits throwInvalidHookError (442-451,3870-3896).

This is the literal mechanism that makes 'read state from outside the render tree' throw. The public API (react) and the engine (react-reconciler) are deliberately decoupled — they share no direct import edge and communicate only through this one mutable slot. There is no second, render-independent read path.

dispatcherrender-onlyReactSharedInternals

The npm state-library ecosystem is downstream of this one decision

observationhighinferred

Redux, Zustand, Jotai, Recoil, Valtio and MobX all keep the source of truth in plain JS outside the fiber and re-enter React through the useSyncExternalStore boundary (use-subscription is now a thin wrapper over it; react-cache was an earlier external-held experiment). They are 'workarounds' only in the sense that they live where addressable, externally-readable state is actually possible.

Framed structurally, these libraries are not patching a missing feature — they occupy the one location (outside the render tree) where shared, externally-readable state can safely exist, and use the one sanctioned door (useSyncExternalStore) to bring it back in. The 'absence' the question names is the negative space of a deliberate boundary.

ecosystemreduxzustandjotaiexternal-store

State is born at mount and destroyed at unmount

observationmediumverified

On unmount, detachFiberAfterEffects sets fiber.memoizedState = null and severs the alternate (ReactFiberCommitWork.js:1348). State is lazily created on first render and torn down with the instance — it cannot pre-exist the mount or outlive the unmount.

No instance means no state. So even setting aside the render gate and tearing, there is no lifetime window in which 'the component's state' exists as an addressable thing outside an active fiber. State that must outlive a component has to live somewhere React does not own.

lifecycleunmountgc

The one place React reads hook state externally — it fakes a render

observationmediumlikely

DevTools walks the Fiber tree and hook state via react-debug-tools (board edge devtools->react-reconciler). It cannot read hooks statically; it re-invokes the component with an instrumented dispatcher to replay the hook calls in order, then reconstructs the values.

The exception proves the rule. Even React's own tooling has no static read path into hook state — to inspect it from outside it must simulate the very render pass the dispatcher gate requires. That is the closest thing to an 'external read primitive' that exists, and it works only by impersonating a render.

devtoolsreact-debug-toolsdispatcher

Want this view of your own system?

ContextDx maps your architecture from your codebase and reconciles it into living, shareable insights — just like this board.