Skip to content

How React Works

Every earlier module in this course taught you to use React: hooks, state, Context, Suspense, Server Components. None of it required knowing what React does with your function calls after you write them. You can write a working useEffect, a correct list with keys, a <Suspense> boundary — without ever asking how React decides when your component function runs, why one update can interrupt another, or what exactly happens between “I called setState” and “the screen changed.”

That gap is exactly where AI-generated React fails in ways that look fine on the surface: a component defined inside another component’s render, an index used as a key on a list that reorders, a useLayoutEffect that fights the browser, a useEffect that assumes it always runs after paint. None of these are syntax errors. None of them are caught by tsc. All of them are facts about the runtime.

This module has one job: give you the five pieces of runtime knowledge that let you look at someone else’s React code — not run it, not profile it, just read it — and answer:

  1. Will this remount or update in place? (fibers, the work loop, reconciliation)
  2. Will this block the next keystroke, or can React interrupt it? (lanes, scheduling)
  3. Which of two similar-looking children keeps its state? (reconciliation’s type + key rules)
  4. In what order do my refs, useInsertionEffect, useLayoutEffect, and useEffect actually fire? (the commit phase)
  5. Why did the page just show a hydration error, and what does React do about it? (hydration and streaming)

Every claim in this module is backed by one of three things: react.dev, the actual React source on github.com/facebook/react (fetched from the main branch, cited by file and function name), or a test in this course’s harness whose real output is pasted below it — never a guess about internals.

LessonAnswers the question
Fiber & the Work LoopWhat is a fiber, and how does React walk the tree to build one?
Lanes & SchedulingHow does React decide an update’s priority, and how does startTransition change it?
Reconciliation in DepthGiven an old tree and a new one, exactly which nodes does React keep, move, or throw away?
Commit Phase & EffectsOnce React has decided what changed, in what order does it touch the DOM and run your effects?
Hydration & StreamingHow does a server-rendered page become interactive, and what happens when the client disagrees with the server?

The Rendering & Reconciliation lesson already introduced the render/commit split and proved the type-vs-key rules from the outside (mount/unmount counts). This module goes past the outside view into the mechanism that produces those counts, and links back rather than repeating the proofs. Transitions already showed useTransition’s user-facing API; Lanes & Scheduling shows what a “lane” actually is underneath it.

A runnable question: does this component update in place, or start over?

Section titled “A runnable question: does this component update in place, or start over?”

Before any of the deeper mechanism, here is the single fact this whole module orbits: React decides “same thing, update it” vs. “different thing, throw it away and rebuild” by comparing an element’s type to the fiber currently sitting in that position — nothing more, nothing less.

src/__tests__/module-preview.test.tsx
import { useEffect } from 'react';
import { render, cleanup } from '@testing-library/react';
import { afterEach, expect, test } from 'vitest';
afterEach(cleanup);
test('same type across renders: no mount, no unmount, just a prop update', () => {
const log: string[] = [];
function Row({ label }: { label: string }) {
useEffect(() => {
log.push(`mount:${label}`);
return () => { log.push(`unmount:${label}`); };
}, []);
return <li>{label}</li>;
}
const { rerender } = render(<Row label="a" />);
log.length = 0;
rerender(<Row label="b" />); // same component, new prop
expect(log).toEqual([]); // proves: React updated the existing fiber in place
});
✓ same type across renders: no mount, no unmount, just a prop update
Test Files 1 passed (1)
Tests 1 passed (1)

Every lesson in this module is really an answer to “why does that rule hold, and what else follows from it.”

flowchart TD
  trigger["Update triggered
(setState, startTransition, hydration)"] --> lane["Lane assigned — priority
(Lanes & Scheduling)"]
  lane --> render["Render phase
beginWork top-down / completeWork bottom-up
interruptible via shouldYield
(Fiber & the Work Loop)"]
  render --> recon["Reconciliation
diff by type, then by key
(Reconciliation in Depth)"]
  recon --> commit["Commit phase
before-mutation -> mutation -> layout -> PAINT -> passive
(Commit Phase & Effects)"]
  commit --> current["Work-in-progress tree becomes 'current'"]
  entry["Server sends HTML"] --> stream["renderToPipeableStream ... hydrateRoot
selective hydration, mismatch recovery
(Hydration & Streaming)"]
  stream --> render
One update, start to finish

Reading unfamiliar React well means running a small machine in your head while your eyes only see JSX: what type is this, what key does it have, what lane is this update on, and which phase of the commit am I in. The five lessons here are exactly those four questions, one lesson each (fiber/work-loop covers “what type, and how does React get there”; lanes covers “what priority”; reconciliation covers “same key, same instance”; commit covers “which phase, in what order”; hydration covers what happens when the very first render already has DOM to reconcile against).

  • “Internals are for people who want to contribute to React.” You need enough of this to read a diff, not to reimplement the reconciler. Every fact in this module is something you can point to in someone else’s pull request.
  • “If it compiles and renders correctly once, the code is fine.” A component defined inside another component’s render, a useLayoutEffect that measures before a ref attaches, an index key on a reorderable list — all render correctly on the first pass. The bug only shows up on the second render, exactly the pass a quick manual test skips.
  • “React’s internal behavior is undocumented magic.” Nearly everything in this module is either stated on react.dev or directly readable in packages/react-reconciler — it’s unfamiliar, not undocumented.

The whole point of this module is this section, five times over — one per lesson. As a preview, here is the survey-level version: a snippet that renders correctly the first time and is still wrong.

🐛 Spot the bug

function ProductList({ products }) {
function ProductCard({ product }) {
return <li>{product.name}</li>;
}
return (
<ul>
{products.map((p) => <ProductCard key={p.id} product={p} />)}
</ul>
);
}
Show the answer

ProductCard is defined inside ProductList’s render body. Every time ProductList renders, this creates a brand-new function — a new type — even though the code looks identical. React’s reconciler compares an element’s type (the function reference itself) to what was there before; a new function reference means “this is a different component,” so every ProductCard unmounts and remounts on every single ProductList re-render, key or no key. Any state inside ProductCard resets every time. The fix is to move ProductCard to the module’s top level (outside ProductList), so type is the same function reference across renders. Fiber & the Work Loop and Reconciliation in Depth prove exactly this mechanism with a test.

What is this module's central goal?
What single comparison does React use to decide "update this fiber in place" vs. "throw it away and rebuild"?
Why does Child remount on every render of Parent, even though the code inside it never changes?
function Parent() {
  function Child() { return <p>hi</p>; }
  return <Child />;
}
True or false: every fact in this module is either stated on react.dev, quoted from the React source with a file and function name, or proven by a test whose real output is shown.
Which lesson explains why one update (like a fast list filter) does not block a more urgent one (like the next keystroke)?