◐ react · from first principles
An interactive deep dive

React is just objects
& functions, arranged
to be described.

No magic, no syntax the browser understands. Underneath every component is a plain JavaScript object and a loop that turns those objects into pixels. We build the whole idea from scratch — and show exactly why vanilla JavaScript ran out of road.

// the entire thesis, in one line
UI = f(state)  — you describe what; the runtime computes how.
Act I · The Abstraction
Act II · The Loop That Computes the How
Act III · The ~100 Lines That Were Enough
Act IV · The Real Codebase
01
The Imperative DOM Tax

What's so hard about updating the DOM by hand?

It isn't the verbosity. It's that you own the job of keeping the in lockstep with your state — by hand, forever, per feature. Build a counter the honest way and the hard part stays hidden; add a second feature and it surfaces.

Here's a counter built the honest, way. It works. Now toggle features and watch the sync code grow.

specimen 01 — imperative.jsmanual sync

The code you must write


          
+ parity badge + reset button + disable at 0

It runs — but at what cost?

manual sync points3
3
lines that exist only to push state → DOM
declarative costflat
~0
in React you'd just describe the new markup

Look closely at what the imperative version actually is. Half of it isn't logic — it's bookkeeping. For every piece of state that you hand-write, the path that carries a change into the DOM looks like this:

  • find the node, then
  • read the old value, then
  • compute the new one, then
  • write it back.

You aren't describing the interface. You're describing the transitions between interfaces, one edge at a time.

And the transitions are where the bugs live. A counter has one state, but a screen with a toggle, a filter, and a modal has a combinatorial number of paths between states — and you are personally responsible for every one. Miss the badge when the count hits zero and the screen now disagrees with the truth. Nothing threw. Nothing crashed. The DOM has just quietly drifted out of sync with your state, and it stays wrong until someone notices.

And those paths multiply faster than you'd guess. Every imperative edit silently assumes a starting state — el.classList.add('active') is only correct if the element exists, wasn't already active, and nothing else is about to remove it. The edit encodes a path, and a path is valid from only one starting point. The instant your app has more than one possible starting state — and it does, the moment you add async, input, or partial failure — you need a different correct path from each. Declarative collapses that fan-out into a single function: one description, correct for every state.

IMPERATIVE · A PATH PER STATE N states → N(N−1) paths DECLARATIVE · ONE FUNNEL f UI N states → 1 function

Fig.On the left, each starting state demands its own hand-authored transition, and the tangle grows faster than the states do. On the right, every state passes through the same function. You stop maintaining a map of routes and maintain a single description of the territory.

◆
Reveal

You are the runtime. The function that maps state to pixels already exists in the imperative version — but it lives in your head, and you execute it by hand, every time, at every edge. Vanilla JS never ran out of power; it ran out of you.

The tax: every feature adds a place where the DOM can drift from the truth (your state). Bugs don't live in the features — they live in the sync code between them.
If that last line rings true from your own work — that the gnarliest bugs hide in the glue between features, not the features — you already feel why the rest of this page exists.
02
The Declarative Leap

UI = f(state)

What if you never touched the DOM? You write one pure function — given the state, here's what the screen should be — and hand the mutation work to someone else. That's the pivot.

Change the state. The imperative mind asks "which nodes do I edit?" The declarative mind just re-states the whole picture as a function of count.

specimen 02 — describe(state)state = { count: 0 }

The single source of truth

0
You changed one number. You touched zero DOM nodes.

What you wrote (re-runs on every change)


          
↻

This function is called again from scratch on every change. Its output is a fresh description, not a list of edits. Re-render = re-describe.

Here is the entire move, in one sentence: stop writing the edits, start writing the answer.

Imperative code is a list of verbs — create, append, setAttribute, remove. It describes a journey: how to drag the DOM from whatever it is now to what it ought to be. Declarative code is a single noun — given this state, here is what the screen is. You stop choreographing the transition and start naming the destination. The route from here to there stops being your job and becomes the runtime's.

You already trust this exact trade everywhere else you work. You don't open a cursor and walk rows mutating them one by one — you write SELECT … WHERE … and let the planner choose the access path. You don't tell the furnace when to switch on — you set a target temperature and let the thermostat converge. UI = f(state) is that same bargain, struck for the screen: declare the result, surrender the steps.

IMPERATIVE · THE JOURNEY DOM₀ node node′ node″ node‴ = target createElement setAttribute appendChild className remove + insert DECLARATIVE · THE DESTINATION state f UI one mapping. no path.

Fig.The left side is a recipe — every arrow is a verb, and every verb assumes the one before it succeeded. The right side is an equation. The contrast in busyness is the whole point: one shape has steps you can get wrong; the other has a result you can only get right.

Here is the part that one sentence glosses over, and it's the most important part. When you write UI = f(state), the create and append and setAttribute calls don't disappear. They still run. Someone still has to walk the DOM from where it is to where your function says it should be. You just stopped being that someone.

In React, f returns a description — a tree of plain objects, not actual DOM. Then the takes your new description, compares it against the previous one, computes the smallest set of mutations that closes the gap, and emits exactly those imperative operations. The verbs you stopped writing reappear at the bottom of the pipeline — authored by the runtime instead of by you.

Declarative isn't the absence of imperative steps. It's the relocation of them — from your code into an engine you've agreed to trust.
YOU WROTE THIS setState(s)an event f(state)your function new element treea description THE RUNTIME — THE PART YOU DELEGATED reconciler: diff(old, new) finds the shortest route from current DOM to target …AND THE VERBS COME BACK createElement setAttribute appendChild remove

Fig.The same verbs from the first diagram reappear at the bottom — but now the runtime emits them, computed fresh from a diff. You wrote a description (teal); the engine inside the dashed box turned it back into a journey (amber). The abstraction didn't delete the imperative work — it moved it somewhere you no longer have to look.

⇄
Comparison

There are two ways to keep a screen in sync with your data. The imperative way — the jQuery way — is to hold references to DOM nodes and hand-write each change: appendChild, textContent = …, removeChild. You own every transition, and that bookkeeping is where bugs breed. The declarative way stops managing transitions altogether: on every change you describe what the UI should be right now, as a pure function of state — UI = f(state) — and hold a handle to nothing. Done naively against the real DOM that would be far too slow. React's trick is to let you write in that all-at-once style while diffing your new description against the last one and touching only what actually changed. The simple model and the cheap implementation — no tradeoff.

Why this deletes a whole genus of bug: a declarative UI is convergent — the same state always produces the same screen, regardless of what was on screen a moment ago. An imperative UI is path-dependent: the DOM is the running total of every edit you've ever applied, so a bug can hide in the history of interactions rather than the current state. Convergence means you can stop debugging time.
You already trust this exact trade elsewhere — SELECT … WHERE, a thermostat's target temperature. Naming the screen's version of it is the entire leap; everything after this is just how React makes it cheap.
03
The POJO Reveal

What's the lightest possible stand-in for a DOM node?

To diff "what should be" against "what is," the runtime needs a representation of your UI it can mint by the thousand and throw away a millisecond later. A real node is far too heavy for that — it has identity, lifecycle, hidden state. So React reaches for the cheapest thing that can still describe a node.

Hover the element or the object keys — same thing, different clothes.

specimen 03 — element ≡ objecttypeof === "object"
●Ready to render

          
↑ a real , console.log'd. $$typeof is a Symbol React stamps on things it made — and it's a Symbol on purpose: Symbols don't survive JSON.parse, so a JSON payload from a server can never forge an element React would render. It's an XSS guard, not decoration.

Notice what just happened to the word "view." A DOM node is a thing: it has identity, it persists, it hoards hidden state — scroll position, focus, a half-typed input — and the only way to change it is to reach in and mutate it. A React element is a value: a plain, frozen description with no identity and no lifecycle, cheap enough to mint a few thousand of on a single keystroke and discard a millisecond later.

That swap — from a mutable thing you hold to an immutable value you produce — is the quiet center of the whole paradigm. Values can be compared, so you can diff them. Values can be kept, so you can hang on to the previous one. Values can be recomputed from scratch, so "what should the screen be?" finally has an answer that doesn't depend on what it was. You can do none of that with a live <div>; you can do all of it with { type, props, children }. The view stopped being infrastructure you manage and became data you compute.

◆
Reveal

A plain object you can build a whole tree of per render and throw away — cheaply enough that re-describing everything becomes a viable strategy, impossible with live, stateful DOM nodes. That disposable sketch the runtime diffs before touching the page is the ; each node is just a of { type, props, children }. The view stopped being infrastructure you manage and became data you compute.

Not a speed trick. The virtual DOM is often slower than hand-written, surgical DOM updates — diffing isn't free. Its real win is the programming model: you get to write UI = f(state) and never reason about transitions, while the runtime makes "fast enough" the default instead of something you hand-tune. Trading peak performance for a model you can actually keep correct.
Hold onto one swap — a mutable thing you hold became an immutable value you produce. Every later movement is something you can only do because the view is now data.
04
JSX, Undressed

Are you really writing HTML in JavaScript?

The persistent myth: "I'm writing HTML in JavaScript." You're not. is syntax a rewrites into ordinary calls — which return the plain objects from 03.

Edit the JSX and watch it flow through the real pipeline, or press play to walk the four stages: your syntax → createElement calls → a plain object → rendered DOM.

specimen 04 — the transpile pipeline (real Babel)booting babel…
try:
→

1JSX — what you write

→

2createElement — Babel's output

→

3the value — a plain object

→

4rendered DOM

JSX feels like magic because it looks like a template language — Handlebars or ERB smuggled into JavaScript. It is nothing of the sort. There is no template engine, no interpolation runtime, no special evaluation context. Every JSX expression is just a JavaScript expression that happens to evaluate to one of those plain objects from movement 03. <App/> is a function call in a costume.

That's why there's no template syntax to memorize for loops or conditionals — you reach for map and && and ternaries, because you never left JavaScript. A component isn't a template that gets "rendered"; it's a function that takes props and returns a value. Once the costume comes off — JSX → createElement → object — the question stops being "how do I do X in JSX?" and becomes "how do I compute this value in JS?" — which you already know how to answer.

Stages 2 and 3 are the same information twice: createElement("h1", null, "Hi") is a recipe that returns { type:"h1", props:{ children:"Hi" } }. JSX → call → object.
▲
Seam

"It compiles away" has a price: browsers run JS, not JSX, so something must parse your source, swap every tag for a function call, and print plain JS before anything ships. That's Babel/SWC/tsc — an honest text transformation. The sugar is free to write; it is never free to run.

What's actually emitted today: we show the classic transform — React.createElement(…) — because it's the clearest. Since React 17 the default is the automatic runtime: the compiler imports jsx/jsxs from react/jsx-runtime and calls those instead, so you no longer need React in scope. Different function name, identical idea — JSX still becomes a call that returns a plain object.
Once the costume comes off, the question stops being "how do I do X in JSX?" and becomes "how do I compute this value in JS?" — which you already know how to answer.
05
Reconciliation — the diff

How does a whole new tree become just a few DOM edits?

Re-render hands React a brand-new object tree. It never trusts it blindly — it : diffs new against old and emits a small set of DOM ops to close the gap.

Pick a state change, then scrub the four lenses (or tab them). The Reconciler tab is the actual ~40-line source running here. Note how a big-looking change often costs one op.

specimen 05 — diff(prev, next) → ops[]keyed: on
state change:
1 real DOM operation(s) scenario: counter +1
prev — on screen
next — your function's output
legend: cyan updated · green added · red removed · amber moved · dim untouched.
This list is the "how" you never write by hand — derived by comparing two plain objects.
prev
next
Only the flashing nodes were touched. Everything else is the same physical node as before.
Honest caveat: this is a heuristic, not a provably minimal diff. A truly optimal tree-edit is O(n³) — too slow for a UI. React makes two assumptions instead — different element types produce different trees, and key marks stable children — to diff in O(n). So the patches you see here are small and good enough, not mathematically smallest. The toy reconciler below makes the same trade.
⤓
Going deeper

Why is that “provably minimal” diff too slow to use — and what two assumptions let React do it in O(n)? Walk the three stages ↗

Hold the shape, not the line count: two trees in, a small patch out. The next movement is what happens when one of those "stable children" assumptions meets a list that reorders.
06
Keys & Identity

Why does React keep asking for a key?

You won't get far rendering a list before React scolds you: "Each child in a list should have a unique key prop." Annoying. So you type key={index}, the warning disappears, and you move on. Then one day the list reorders — an input shows the wrong value, a checkbox checks the wrong row.

Let's make that happen on purpose. It's the fastest way to see what a actually buys you.

⌨

Do this: type into the first input of both lists (e.g. "MY TEXT"), then hit Prepend. Watch where your text lands.

specimen 06 — same change, two strategies

Without keys

// children matched by index
—

With keys

// children matched by key=id
—
What happened: keyless, your text stayed put while the labels slid out from under it — React reused node #0 and rewrote its text. Keyed, the text traveled with its row. React decided your text belonged to a position, not a row.

Why does it default to position? It comes down to one question React asks about every child on every render — and it's worth watching it decide.

◆
Reveal

A key is a promise of identity across renders.

◈
Pragmatic

key={index} isn't wrong — it's genuinely fine when a list never reorders, filters, or splices. There's no identity to confuse. It only bites the moment positions and items stop lining up.

▲
Seam

That promise has a price inside the engine. While children only match by position, the reconciler can always compute where a node lives — “the child at index i.” The moment a key lets a node move, that arithmetic dies: React must instead carry a pointer to each element's real node, wherever it ends up. That shift — identity as a stored pointer, not a computed position — is the first plank of the fiber tree you'll meet in movement 10.

If you've been bitten by this before, you now know exactly why — which puts you ahead of most people, who sprinkle in key={index} and hope.
07
State & the Re-render Loop

What actually happens the moment state changes?

Everything so far connects here. hands you a value and a way to say "I changed it, run me again." Running again makes a new object tree → reconciled → patched. One loop, forever.

Step back and look at the shape of the whole thing. In movement 01, you were the runtime: you held the state→DOM mapping in your head and re-ran it by hand at every edge. Every movement since has been quietly lifting that job out of you and handing it to code. useState holds the state you used to track yourself. The render function is the mapping you used to run in your head — now written down exactly once. Reconciliation is the by-hand patching from 01, generalized into forty-odd lines. The loop below is simply that machinery, turning. You didn't surrender control; you named the thing you were already doing, and let the computer do it faster and without forgetting.

specimen 07 — the cyclescrub or play ▶
1 · state
0
→
2 · render(state)
·
→
3 · new vDOM
·
→
4 · reconcile
·
→
5 · patch DOM
·
"Call the function again" — with three asterisks. It re-runs the component and its subtree, not the whole app; React.memo and bailouts can prune even that. Multiple setState calls are batched into one pass rather than one-per-call. And updates are scheduled, not synchronous — the "new value" isn't readable on the very next line. The five-step loop is the true shape; production just adds when and how much on top.
⤓
Going deeper

That marginalia just made three claims on faith — batched, scheduled, “not readable on the next line.” The cell sketch below can't produce any of them: it writes the cell the instant you call the setter. What has to change? Walk the three stages ↗

So what is useState, mechanically? A value in a cell plus a scheduler callback. Three views:

useState, demystified
useState(0)returnscount = 0 — the value for THIS rendersetCount — ask React to re-run the componentsetCount never writes to the DOM — it kicks off the loop (tap any stage):setCount(n)re-run Counter()new vDOMdiffpatchreal DOM
fires the render loop — tap to run
useState(0)useState("dark")cell 0cell 1cell 2…cellscursor
cursor at 0 — no hook has run yet
two useState calls, matched to cells by order:useState("a")useState("b")cell 0 = "a"cell 1 = "b"slots aligned ✓
both hooks run — each reads its own slot
YOUR COMPONENT · one render function Counter() { const [count, setCount] = useState(0) const [theme, setTheme] = useState("cyan") } cursor = 0 cells — kept between renders [0] count 0 [1] theme "cyan" setCount(1) → writes slot 0, then re-renders render() resets cursor = 0, then re-runs Counter()

Fig.A hook isn’t magic — it’s an index into a per-component array. Each useState claims the next cell by call order; setState writes to that cell and re-runs your function, which reads the same cells again. Keep the call order identical and the cells always line up. Tap Step to watch one full turn.

render 1 · on = true useState("a") useState("b") cell[0] = "a" cell[1] = "b" cursor 0 cursor 1 a = "a"  ·  b = "b" slots aligned ✓ render 2 · on = false useState("a") useState("b") cell[0] = "a" cell[1] = "b" cursor 0 → wrong slot b = "a" ✗ every later hook shifts a slot

Fig.Hooks bind to state by call order, not by name — the Nth useState always reads the Nth cell. Put one inside an if and a render where it’s skipped renumbers every hook after it, so they read the wrong cells. That’s the whole reason for the rule: same hooks, same order, every render. Tap Step.

Where the cells really live. One module-global cells array would collapse the moment you mounted the component twice. React keeps a separate hook list per component instance, stored on that instance's internal node (its fiber), and swaps the right one in before each render. The mechanism you care about is unchanged — fixed slots, matched by call order — but it's per-instance, not one global array.
setCount(count + 1) · called 3× all three read the same snapshot — count = 0 setCount(count + 1) 0 + 1 = 1 setCount(count + 1) 0 + 1 = 1 setCount(count + 1) 0 + 1 = 1 next render count = 1 setCount(c => c + 1) · called 3× each call gets the latest pending value setCount(c => c + 1) 0 → 1 setCount(c => c + 1) 1 → 2 setCount(c => c + 1) 2 → 3 next render count = 3 ✓

Fig.State is a per-render snapshot: inside a single render, count is a constant. Calling setCount(count + 1) three times all reads the same value, so you get +1 — while setCount(c => c + 1) threads each result into the next, giving +3. Tap Step.

◆
Reveal

A re-render is just "call the function again." You didn't surrender control to React — you named the state→DOM mapping you were already running by hand in movement 01, and let the machine turn the loop faster and without forgetting.

The loop you just watched is the real one. Everything production adds — batching, memo, scheduling — is a refinement of when and how much, layered on this exact five-step shape.
08
Build-Your-Own-React

Does the whole runtime really fit in ~100 lines?

Every piece is now on the table: elements as objects, a reconcile, a render loop, per-instance hooks. Here they are assembled into a real runtime — and the source on the left is literally the code running the app on the right (printed straight from the live functions via .toString()).

specimen 08 — mini-react.js, mounted~100 lines
mounted & live — every click goes through your reconcile
lifecycle log
Read it top to bottom. createElement is movement 03–04. reconcile is movement 05. useState is movement 07 — now with the per-instance hook record from that section's caveat. Nothing here is pseudocode; it's the whole runtime, and it fits on one screen.
Everything after this point is the same machine under load — what production adds when one synchronous pass over a big tree is no longer fast enough to stay out of the user's way.
09
Effects & the Commit Phase

Why does React build the whole patch before changing anything?

Your component function runs in the — pure, no DOM touched, safe to discard or re-run. Only once React is sure does the apply the patch. run after that, once the browser has painted.

specimen 09a — the two phasesscrub or play ▶
trigger
setState()
a value changed; schedule work
render
render phase · pure
call Component()
build a new vDOM tree · no DOM, no effects · discardable
render phase · pure
reconcile(prev, next)
compute the patch — still touching nothing real
commit
commit phase · mutating
apply patch → DOM
the one place the real DOM changes · synchronous, all-at-once
browser
paint
pixels update on screen
after paint
run effects
cleanup the previous effect, then run the new one
Why the split matters: because render is pure and discardable, React can render a tree, throw it away, and render again (different priorities, Suspense, concurrent) without ever leaving half-mutated DOM on screen. Side effects in render would make that impossible — which is the whole reason they live in useEffect.

Feel the dependency array decide. The theme below is an effect dependency; the counter is not. Watch which changes fire the effect.

specimen 09b — useEffect(fn, [deps])live, on the mini-react
component
effect log
Changing the counter re-renders but the deps are equal → effect skipped. Changing the theme → old effect's cleanup runs first, then the new effect.
⤓
Going deeper

The timeline above says effects run after paint — yet the mini-react driving that very demo runs them at commit, and nothing looked wrong. So when does the difference bite — and why does React ship two effect hooks? Walk the three stages ↗

◆
Reveal

Render is a draft; commit is the ink. Because the draft is pure and discardable, React can build it, throw it away, and build it again — at different priorities, or paused mid-way — and the real DOM only ever sees the finished, all-at-once patch.

That "discard the draft and start over" freedom looks academic here — but it's the exact property the next movement spends itself buying, because our diff so far can't actually be paused.
10
Fiber — the interruptible diff

The diff you built can't be paused. That's the problem.

Our reconcile recurses the whole tree in one synchronous call. For a big tree that's a long task that blocks the main thread — no input, no paint, until it finishes. rewrites the walk as a that can stop and resume.

specimen 10a — recursion vs. a work loop

recursive — one unbreakable call

function reconcile(parent, old, next) {
  // ...patch this node...
  for (let i = 0; i < n; i++)
    reconcile(node, old[i], next[i]); // ← can't pause
}
// the call stack IS the position.
// you cannot return to the browser
// halfway down without losing it.

fiber — position is data, not stack

let next = root;             // a pointer
function workLoop(deadline) {
  while (next && deadline.timeRemaining() > 1)
    next = performUnit(next);  // do ONE node
  if (next) requestIdleCallback(workLoop); // resume later
  else commit(root);           // all done → commit once
}
// each fiber links to child / sibling / return,
// so the walk can stop after any single unit.
The trick: a fiber node stores pointers — child, sibling, return (parent) — so the traversal state lives in the data, not the call stack. The loop can hand control back to the browser after any unit and pick up exactly where it left off.
specimen 10b — block the thread, or slice the worktry typing during each
work units:20000
responsiveness probe:
blocking — single sync taskidle
One call. The spinner freezes and your keystrokes queue up until it's done.
time-sliced — yields between chunksidle
Same work, in chunks. The spinner keeps spinning and typing stays live.
This is the honest end of the thread: Fiber doesn't make the diff less work — the sliced run is slightly slower in total. It makes the work interruptible, so high-priority things (typing, animation) can cut in. That capability is what unlocks concurrent features, Suspense, and transitions — the rest of React, built on the loop you now understand end to end.
⤓
Going deeper

Pausing is only half the trick. When the walk stops midway — or gets thrown away for something more urgent — where has the half-finished tree been living? It can't be the one the screen believes in. Walk the three stages ↗

◆
Reveal

Make position data, not stack. Once the walk's place lives in child/sibling/return pointers instead of the call stack, the diff can stop after any single node and resume later — which is the one thing recursion could never do.

⤓
Going deeper

The marginalia above tossed off a claim — that all this “unlocks Suspense.” Cash it: a component deep in the tree discovers, mid-render, that its data hasn't arrived. What can it possibly do from down there? Walk the three stages ↗

You've now built the whole engine and watched it learn to yield. The final movement is just where each piece of it actually lives in the real source.
11
The real codebase — react vs react-dom

Where does each piece you built actually live?

Everything so far lived in one file. The real React is a monorepo that cuts the same machine along its seams: one package describes UI, one turns descriptions into a platform, and the engine in between is shared by every renderer. Here's the map, and what each function actually does.

You install two packages — react and react-dom — and it's tempting to think of them as one thing. They are deliberately not. react is the description layer: it knows how to make element objects and expose hooks, and it knows nothing about the DOM. react-dom is the renderer: it turns those descriptions into real browser nodes. Between them sits react-reconciler — the Fiber tree, the diff, and the actual hook implementations you reverse-engineered — plus scheduler, which decides when the work runs. The split is what lets the same react drive the browser, native, canvas, or a test renderer without changing a line.

the package map — one render, four packages
your components — <App/> react react.js the description layer · host-agnostic, never touches a DOM node createElement · hooks (facades) · Component · createContext · memo react-reconciler the engine — shared by every renderer Fiber tree · reconcile/diff · work loop the real hooks · commit phases · lanes scheduler decides when work runs priorities · time-slicing shouldYield() ↓ calls the host config react-dom browser DOM + events createRoot · hydrateRoot react-native native host views react-art · test canvas · assertions each renderer supplies a host config: createInstance · appendChild · commitUpdate · removeChild same description, same engine → pixels on whatever platform the renderer targets
The boundary that matters: the cyan layer describes, the green layer paints, and the amber engine — the diff and work loop you built — is shared between them. Swap the bottom row and React runs somewhere new.
⇄
Comparison

react answers “what is the UI?” — import only it and nothing ever appears on screen. react-dom answers “put it in this browser.” Rendering doesn't begin until a renderer's entry point runs: createRoot(node).render(<App/>). React Native imports the same react and a different renderer — that is the entire reason the API is split.

So what's actually in each package? Below is the working set — the functions you call, and the ones underneath that do the work.

packages/react — the description layer
createElement(type, props, …children)
Returns a plain element object — { $$typeof, type, key, ref, props }. Every JSX tag compiles to this call. It only describes a node; it creates no DOM.
jsx / jsxs / jsxDEV
The modern compile target in react/jsx-runtime (the “automatic runtime”). Same job as createElement, but children arrive inside props and the dev build records source location.
cloneElement · isValidElement · Children
Utilities for inspecting and transforming element objects — Children.map/forEach/count/toArray/only safely walk the opaque props.children.
Component · PureComponent
The class component base. They mostly set up this.props/this.state and a setState/forceUpdate that the reconciler wires up. PureComponent adds a shallow-compare shouldComponentUpdate.
createContext(default)
Creates a context object with a Provider and Consumer. The reconciler reads it during render so useContext can find the nearest provider's value.
memo · forwardRef · lazy
Component wrappers the reconciler recognises by their $$typeof: memo skips a re-render when props are shallow-equal, forwardRef passes a ref through, lazy suspends until a dynamic import resolves.
Fragment · Suspense · StrictMode · Profiler
Marker element types with no DOM of their own. The reconciler gives each special behaviour — grouping without a wrapper, fallback boundaries, double-invoking renders to surface bugs, timing measurements.
useState · useReducer · useRef · useMemo · useCallback · useContext · useEffect · useLayoutEffect · useId · useTransition · …
These are one-line facades. In the react package each hook just forwards to the current dispatcher — the real logic lives in the reconciler (next block). That indirection is what keeps react host-agnostic.
use · useActionState · useOptimistic
Newer hooks (React 19): use unwraps a promise or context during render; the others support form actions and optimistic UI. Same facade pattern — they resolve to the dispatcher.
startTransition · version · internals
startTransition marks an update as low-priority. A guarded internals object (historically __SECRET_INTERNALS…) holds the shared dispatcher slot the hook facades read from.
how a hook in react reaches the reconciler
// in the `react` package — the public hook is a thin facade
export function useState(initialState) {
  const dispatcher = resolveDispatcher();   // reads the *current* dispatcher
  return dispatcher.useState(initialState); // ← the real work is in react-reconciler
}

// react-reconciler swaps the dispatcher before each render:
//   first render  → HooksDispatcherOnMount  (mountState, mountEffect…)
//   re-render     → HooksDispatcherOnUpdate (updateState, updateEffect…)
// Outside render no dispatcher is set — which is why calling a
// hook there throws, and why hook order must stay identical (movement 07).
The same useState you call resolves to mountState on the first render and updateState after — the react package never contains either.
packages/react-dom — the browser renderer
createRoot(container, options?)
From react-dom/client. Creates a root tied to a DOM node and returns { render, unmount }. This is where rendering starts and where concurrent features are enabled — the React 18+ replacement for the old ReactDOM.render.
hydrateRoot(container, element)
Like createRoot, but adopts server-rendered HTML already in the container — attaching event handlers and state instead of recreating nodes.
createPortal(children, container)
Renders a subtree into a different DOM node (modals, tooltips) while keeping it in the React tree for context and events.
flushSync(fn)
Forces any updates inside fn to render and commit synchronously before returning — an escape hatch out of batching when you must read the DOM immediately after a state change.
preload · preinit · prefetchDNS · preconnect
React 19 resource hints that let a component declare the stylesheets, scripts, and origins it needs so the browser can fetch them early.
renderToString · renderToPipeableStream · renderToReadableStream
From react-dom/server. Turn a tree into HTML on the server — the streaming variants flush markup progressively and integrate with Suspense boundaries.
createInstance · appendChild · commitUpdate · removeChild · …
The host config. This is react-dom's real contract with the reconciler: a set of functions the engine calls to make things happen in the DOM. Implement this object for a new target and you have a new renderer.
src/events/*
The synthetic event system — one set of delegated listeners at the root that normalises cross-browser differences and dispatches events down the fiber tree, rather than per-node listeners.
packages/react-reconciler — the engine (your diff + work loop, for real)
Fiber (the node)
A unit of work and a node in the work-in-progress tree. It holds the element's type/props, its hook list (memoizedState), the child/sibling/return links you used in movement 10, and effect flags + lanes (priority).
scheduleUpdateOnFiber
The entry point for any update. A setState marks a fiber dirty with a lane and asks the scheduler to do work — the start of every re-render.
workLoopConcurrent / workLoopSync
The loop itself — repeatedly calls performUnitOfWork. The concurrent version checks shouldYield() between units so it can pause; the sync version runs straight through. This is exactly your movement-10 work loop.
beginWork(current, wip, lanes)
The “down” pass on one fiber: runs the component (via renderWithHooks) to get its children, then reconcileChildren diffs them and produces child fibers.
reconcileChildFibers
The keyed diff — the algorithm from movements 05 and 06. It matches old and new children by key, reusing fibers where it can and flagging the rest for placement, update, or deletion.
completeWork(current, wip)
The “up” pass: for a host component it creates or prepares the platform instance (through the host config) and bubbles child effect flags upward, so commit knows exactly what changed.
commitRoot → commitMutationEffects · commitLayoutEffects
The synchronous commit, in sub-phases: mutation applies DOM inserts/updates/deletes via the host config; layout runs useLayoutEffect and attaches refs. This is the “commit” half of movement 09.
flushPassiveEffects
Runs useEffect callbacks (and their cleanups) after paint, asynchronously — the “effects run after the screen updates” rule, enforced here.
renderWithHooks · mountState / updateState · mountEffect / updateEffect
The real hooks. renderWithHooks points the dispatcher at the mount or update table and calls your component; mountState/updateState are the actual useState — the slot-by-call-order machinery from movement 07, per fiber.
Lanes (priority model)
A bitmask of priorities attached to updates. Lanes let the engine interleave urgent work (typing) with non-urgent work (a transition) — the bookkeeping behind interruptibility.
packages/scheduler — and the supporting cast
scheduleCallback(priority, cb)
Queues work at a priority — Immediate, UserBlocking, Normal, Low, or Idle. The reconciler hands the scheduler a callback and lets it decide ordering and timing.
shouldYield()
Returns true once the current task has used its slice of the frame, telling the work loop to pause and resume later — the production version of your deadline.timeRemaining() > 1 check.
MessageChannel loop
Time-slicing is built on a MessageChannel (not requestIdleCallback) for predictable, fast yielding. This package is what makes Fiber's interruptibility real.
react-is
Tiny runtime type checks (isElement, isFragment, …). It is the most-downloaded React package because so many libraries depend on it.
react-server · renderers
RSC/server infrastructure, plus the other renderers on this same engine — react-native-renderer, react-art, react-test-renderer, react-noop-renderer.
react-refresh · react-devtools · eslint-plugin-react-hooks · React Compiler
Tooling: Fast Refresh, the inspector, the lint rule enforcing the call-order law from movement 07, and the optimizing compiler that auto-memoizes components — separate from the runtime.
One click, all four packages: a button press enters react-dom's event system → your setState calls the reconciler's scheduleUpdateOnFiber, tagging the update with a lane → scheduler picks it up and runs the work loop, which calls your component through the dispatcher that react exposes → commitRoot applies the result through react-dom's host config → the browser paints → useEffect flushes after. Four packages, one render — and react itself never touched a DOM node.
◆
Reveal

The seam that organizes the whole codebase is "describe" vs "paint." react describes, a renderer paints, and the engine you built — diff and work loop — is shared between them. Swap the renderer and the same description runs somewhere new.

Nothing in the real source is a different kind of thing than what you built — it's the same machine, split along its seams and hardened. You can open any of those files now and recognize the shape.

open in glossary →
Back to JIT-Atlas