← Back to writings

I understand how React actually works.

February 4, 2026

At some point I realized I'd been writing React for years but couldn't actually explain how it worked. Not the API — I knew the API. But the underlying logic, why it behaves the way it does, what it's actually doing when it "re-renders" something. I just had a vague sense of magic filling in the gaps. Turns out the magic has a pretty clear explanation. This is that explanation.

Why React Feels Confusing at First

If you're coming from plain HTML, CSS, and JavaScript, your mental model of the DOM is direct and physical. You select an element, you mutate it, you see the result. The feedback loop is tight and visible. That model works well enough — until React enters the picture.

Suddenly you don't touch the DOM directly. The UI updates when you didn't explicitly tell it to. Components "re-render" and you're not entirely sure why. And somewhere in the back of your mind, you accept the explanation that most developers eventually accept: React is doing some magic under the hood.

That's exactly where the confusion starts — because React is not magic. It's doing something very specific, very deliberate, and fundamentally different from manual DOM work. The confusion doesn't come from React being mysterious. It comes from carrying the wrong mental model into a system that was built around a different one.

To understand React, you don't need to memorize more hooks or APIs. You need to swap out one foundational assumption about how the browser gets updated.


React Is Not the DOM — It's a Planner

Here's the single most important shift to make:

React does not update the UI by directly manipulating the DOM. At least not first. Instead, React works in two distinct steps — figure out what the UI should look like, and then update the DOM only where necessary.

When you write JSX like this:

<h1>Hello</h1>

React does not reach into the browser and create an <h1> element. What it actually creates is a lightweight JavaScript object — a description of what you want:

{
  type: "h1",
  props: { children: "Hello" }
}

No DOM. No browser involvement yet. Just data.

Think of React as a UI planner: it builds a plan first, then decides when and how to execute it. This separation — between describing the UI and actually producing it — is the foundation of everything else React does. It explains more of React's behavior than any other single concept. Once it clicks, a lot of the confusing things React does start to feel intentional rather than arbitrary.


Components Are Just Functions That Return Descriptions

With that framing in place, components become much less mysterious. Take this:

function Button() {
  return <button>Click</button>;
}

When React processes this, it calls the function, receives a React element back, and adds it to the description tree. That's it. A React component is not a DOM element, not a UI widget, not some special object. It's a function that returns a description of what should appear on screen.

This matters because functions are cheap to call. Their results can be compared. Their work can be discarded and redone without consequence. React leans heavily on all three of those properties.

When something changes — state, props, context — React doesn't panic or immediately run to the DOM. It calls the function again, builds a new description, and compares it with the previous one. Only after that comparison does React even think about touching the browser.

And this is a subtle but important realization: at this point in the process, React hasn't rendered anything yet. All it has is JavaScript objects describing what the UI should look like. The browser is still completely untouched. This is precisely why React can later pause rendering, prioritize some updates over others, and stay responsive under load — it's operating on cheap data structures, not live DOM nodes.


Why React Can't Just "Render Everything Immediately" — and Why Fiber Exists

Up to this point, React has been doing nothing but thinking. It has JavaScript objects, it has functions it can call repeatedly, and it has avoided touching the DOM entirely. So a fair question arises: why not just calculate everything and update the DOM immediately?

Because in real applications, that approach hits a hard wall.

Imagine a UI with hundreds of components, expensive computations, network-driven updates, animations, and user input all happening simultaneously. If React tried to render the entire component tree in one uninterrupted pass every time something changed, the browser would freeze. Typing would lag. Scrolling would stutter. Early versions of React actually worked closer to this model — and they ran straight into that limit.

So React needed a new ability: the ability to stop rendering in the middle, handle something more important, and resume from exactly where it left off.

That single requirement led to Fiber.


Fiber: React's Unit of Work

Fiber is not an API. It's not a feature you import or configure. It's how React internally represents and manages rendering work.

The simplest way to think about it: a Fiber is a small data record that represents one component's work. Every component instance in your application corresponds to one Fiber node. Whereas before Fiber, React rendered the component tree using recursive function calls — which, once started, cannot be paused — Fiber replaces that recursion with a controllable work loop.

To feel why recursion was a problem, think concretely. Imagine React is rendering a tree of 500 components. Halfway through, at component 250, the user presses a key. Old React is stuck. It started the recursive descent and it has no way to stop — the call stack is committed to finishing. That keystroke just sits in a queue, waiting. The user feels the lag.

Fiber fixes this by making each unit of work a node in a linked structure rather than a frame in a call stack. React can do a little work, yield back to the browser, let it handle input or paint a frame, and then resume from exactly the same Fiber node it paused at. Nothing is lost. No state is corrupted. No DOM is left half-updated.

Each Fiber node stores the component it represents, its current props and state, pointers to its parent, child, and sibling, information about what needs to change in the DOM, and how urgent its updates are. It's data plus control plus scheduling information, all bundled together. This is why React can look at any point in a partially-rendered tree and answer the question: "Should I keep going, or is there something more important to do right now?"

Your component — the function Card() you wrote — is not the Fiber. The JSX is not the Fiber. The DOM node is not the Fiber. The Fiber is React's internal work record: here's what I know about Card, and here's what still needs to be done.


Rendering vs. Committing: The Most Common React Misunderstanding

With Fiber as the foundation, React can now render incrementally — pausing, resuming, even discarding work and starting over if something more urgent arrives. But this introduces a distinction that trips up almost every developer at some point.

Rendering does not mean updating the DOM.

In React, rendering simply means: figure out what the UI should look like. The entire render phase — calling your components, building the Fiber tree, comparing old descriptions to new ones — is interruptible. It can run multiple times. It can be paused mid-way. It produces no visible changes because it doesn't touch the browser at all.

Only once React is satisfied with its plan does it move to the commit phase. This is when changes become real: the DOM gets updated, refs get attached, layout effects run, passive effects get scheduled. And unlike the render phase, the commit phase is never interruptible. React waits until it can apply all changes at once, which guarantees that the UI is always consistent — you never see a half-updated screen.

This separation is the key to understanding React's behavior at a deeper level. Because rendering is cheap and non-destructive, React can re-think freely. Because committing is atomic and final, React can act decisively. This is how React can discard renders, retry work, and prioritize updates without breaking your UI — it throws away plans, never partial executions.


Reconciliation: How React Knows What Changed

Every time a component re-renders, React has two UI descriptions: the previous one and the new one. The process of comparing them is called reconciliation, and it's more nuanced than a simple diff.

React walks the Fiber tree and matches elements by their type and their key. When it finds a previous element and a new element of the same type in the same position, it reuses the existing Fiber node and updates only what changed. When it finds a different type, it tears down the old subtree entirely — destroying state, unmounting effects, and building fresh from scratch.

This is why the key prop matters so much in lists. Without a stable key, React uses position to match elements. Move an item in a list and React might think a completely different component changed. With a correct key, React can match each item to its previous Fiber node regardless of position, preserving state and avoiding unnecessary destruction. A missing or unstable key isn't just a performance warning — it can cause genuinely wrong behavior.

Reconciliation isn't purely about speed. It's about correctness with the minimum amount of work. React is trying to figure out the smallest set of DOM mutations that moves the current state of the browser to the desired state. Less DOM mutation means less reflow, less repaint, and a more stable user experience.


Scheduling and Lanes: How React Decides What Matters Most

React now knows how to render incrementally and how to figure out what changed. The remaining question is harder: when multiple updates are pending at the same time, what should React work on first?

This is not a hypothetical problem. In real applications, many things happen concurrently. The user is typing in a search input. Results are being filtered and rendered. A chart is updating in the background. Some data is loading over the network. All of these trigger React updates — but from the user's perspective, they are not equally important. Typing has to feel instant. Everything else can wait.

React's solution is lanes — a labeling system for the urgency of updates. When something triggers a re-render, React doesn't just note that something changed. It also asks how urgent the change is, and places the update into a corresponding lane. Some lanes are fast — direct user interactions like typing or clicking go here. Some lanes are slower — standard state updates from data fetching go here. Some lanes run only when the application is idle.

React always works on the highest-priority lane that has pending work. Lower-priority lanes wait, even if they arrived first. This is intentional. Before lanes existed, React processed updates largely in the order they arrived, which meant a heavy render could block a keystroke. A long list filter could make typing feel sluggish. With lanes, urgent work can interrupt non-urgent work mid-render, handle the urgent task, and then resume the lower-priority work from where it paused.

This is where startTransition fits in. When you wrap a state update in startTransition, you're explicitly telling React: this update is not urgent. React places it in a low-priority lane. If the user types while that transition is rendering, React pauses the transition, processes the typing immediately, and resumes the transition afterward. No state is lost. The UI just feels right.

One thing worth understanding clearly: lanes don't make React faster in absolute terms. They make React smarter about time. The goal isn't to finish everything as quickly as possible — it's to never get in the user's way.

Most of the time, React's lane decisions are invisible to you. You don't need to manage them directly. startTransition is really the only explicit tool you'll reach for regularly, and only when you have an expensive but non-urgent update that would otherwise compete with user input. If you've never touched lanes directly, that's normal. Understanding them isn't about using them — it's about understanding why React sometimes seems to prioritize things in an unexpected order.


Why React Sometimes Renders More Than You Expect

Once you understand the render/commit separation, React's tendency to call components more than you expected stops feeling like a bug.

React treats rendering as pure and repeatable. Because rendering doesn't touch the DOM and produces no side effects, it's essentially free to throw away and redo. If React is uncertain — if Concurrent Mode interrupts a render, if Strict Mode deliberately double-invokes your components in development, if a higher-priority update arrives mid-render — it will simply discard the work in progress and start again.

This is intentional and correct. React would rather re-render three times and commit the right result once than commit something incorrect after a single pass. Rendering is cheap. Incorrect UI is not.

The practical takeaway is that you should never rely on a render having side effects, and you should never assume a render will happen exactly once. If your component is pure — given the same props and state, it returns the same description — React can call it as many times as it needs to, and the result will always be correct.


What React Is Actually Optimizing For

It's worth stepping back and being explicit about this, because it reframes a lot of things.

React is not optimizing for the fewest renders. It's not trying to minimize function calls. It's not racing to produce the fastest possible DOM update. Those metrics would all lead to a system that feels smooth on benchmarks but brittle in practice.

What React is actually optimizing for is a responsive, predictable user experience. Every architectural decision described in this article serves that goal. Planning before acting keeps the DOM consistent. Fiber makes work interruptible so user input is never blocked. The render/commit split ensures changes are atomic. Lanes ensure that urgency determines priority, not arrival order.

Speed matters — but smoothness matters more. React is willing to render more often, discard more work, and run more comparisons than strictly necessary, because the alternative — a UI that freezes, lags, or shows partial states — is worse.


The Mental Model to Keep

The clearest way to summarize how React works:

You describe what the UI should be. React builds a plan — a tree of Fiber nodes representing your components and what needs to happen to each one. React schedules that work by importance, using lanes to decide what to do now and what can wait. When the plan is complete, React commits all changes to the DOM at once, leaving the browser in a consistent, correct state throughout.

React doesn't hide this from you. It just does all the hard thinking before it touches anything.


Why This Makes Everything Else Click

Once this model is internalized, the rest of React stops feeling arbitrary.

Hooks feel logical because they're how components persist state across renders — without hooks, calling a function repeatedly would reset everything to its initial value every time. The rules of hooks (no conditionals, no loops) exist because React tracks hook calls by their order within the component, and that order has to be stable for Fiber's bookkeeping to work.

Performance issues feel debuggable because you can reason about what triggered a re-render, whether the render phase is doing unnecessary work, and whether a commit was avoidable. The mental model gives you the right questions to ask.

Concurrent features — startTransition, useDeferredValue, Suspense — stop being scary because you understand what they're doing: they're communicating urgency to the lanes system, or telling React it's okay to render a fallback while work is in progress.

React is not something to fight or fear. It's a system built around a clear set of ideas. And now you know what those ideas are.