Skip to content
DevDepth
← Back to all articles

In-Depth Article

How JSX Becomes DOM in React: Compile, React Element, Fiber, and Commit

Follow the full React pipeline from JSX compilation to React elements, Fiber reconciliation, and the final DOM commit.

Published: Updated: 9 min readreact-internals

When people explain React rendering quickly, they often say something like "the virtual DOM becomes the real DOM."

That direction is not completely wrong, but it skips several important transformations in the middle.

If you want the more accurate version, the pipeline looks like this:

  1. JSX is compiled into JavaScript function calls
  2. Those calls produce React element objects
  3. React reconciles those elements into a Fiber tree and marks the required work
  4. The renderer commits the result to the real DOM

So the real story is not just "virtual DOM to DOM." It is:

JSX is syntax sugar, React elements are lightweight descriptions, Fiber is the working data structure, and the commit phase is where React DOM finally touches the browser.

This article walks through that full path from the bottom-up data structure point of view. If you want the adjacent mental models too, this pairs well with how React updates move from setState to a DOM update, how concurrent rendering is built on Fiber and scheduling, and why useEffect and useLayoutEffect differ during commit.

1. The four-stage mental model

Before diving into implementation details, it helps to name the four stages clearly.

Stage 1: Compile

JSX becomes plain JavaScript.

Stage 2: Runtime

The generated JavaScript runs and creates React element objects.

Stage 3: Reconciliation

React compares the new element tree with the current Fiber tree, builds a work-in-progress tree, and marks what needs to happen.

Stage 4: Commit

The renderer applies those marked changes to the host environment, such as the browser DOM.

That sequence matters because each stage has a different job:

  • compilation handles syntax
  • runtime builds descriptions
  • reconciliation calculates work
  • commit performs visible mutations

2. Stage 1: JSX is only syntax sugar

Browsers do not understand JSX directly. A line like this:

<div className="app">
  <h1>Hello</h1>
</div>

cannot run in the browser as-is.

Before your code ships, a compiler such as Babel or SWC rewrites JSX into normal JavaScript calls.

Before React 17: React.createElement

In older output, JSX typically became nested React.createElement(...) calls:

React.createElement(
  "div",
  { className: "app" },
  React.createElement("h1", null, "Hello")
);

React 17 and later: the new JSX runtime

With the newer JSX transform, the compiler can emit helpers from react/jsx-runtime instead:

import { jsx as _jsx } from "react/jsx-runtime";

_jsx("div", {
  className: "app",
  children: _jsx("h1", { children: "Hello" }),
});

The key point is the same in both cases:

JSX does not create DOM nodes. It compiles into function calls that describe what React should render later.

That is why JSX should be treated as developer-friendly syntax, not as a runtime feature provided by the browser.

3. Stage 2: those function calls create React elements, not DOM nodes

Once the compiled JavaScript runs, React executes React.createElement, jsx, or jsxs.

This step is fast because React is still not doing any real DOM work.

Instead, React builds plain JavaScript objects. These objects are usually called React elements. In casual React discussions they are often lumped into "virtual DOM," but the more precise source-level term is ReactElement.

A simplified shape looks like this:

const element = {
  $$typeof: Symbol.for("react.element"),
  type: "div",
  key: null,
  ref: null,
  props: {
    className: "app",
    children: {
      $$typeof: Symbol.for("react.element"),
      type: "h1",
      key: null,
      ref: null,
      props: {
        children: "Hello",
      },
    },
  },
};

This is only a description object saying:

  • render a div
  • give it className="app"
  • put an h1 inside it
  • give that h1 the text Hello

At this point, nothing has been inserted into the page yet.

That leads to one of the most useful corrections you can make in interviews or code reviews:

React.createElement does not create DOM. It creates a lightweight object that describes desired UI.

Where function components fit into this stage

For host elements such as <div />, the React element type is a string like "div".

For a component such as <App />, the React element type is the component itself:

const element = {
  $$typeof: Symbol.for("react.element"),
  type: App,
  props: {},
};

That does not mean React creates DOM for App directly.

During reconciliation, React executes the component function to get the next layer of React elements that App returns. Those returned elements may include more components, host elements, or both.

So an important refinement is:

Component React elements describe "run this component to get child output," while host React elements describe "eventually create or update this host node."

4. Why React cannot stop at React elements

If React elements are already describing the UI, why does React need anything else?

Because rendering is not just about storing a desired tree. React also needs to:

  • compare old and new output
  • track component state
  • record pending updates
  • prioritize work
  • pause and resume rendering in concurrent mode
  • remember which host changes should happen in commit

A React element object is too light to do all of that by itself.

That is why React uses Fiber as its real working structure.

If you want the deeper concurrency angle behind this design, see React concurrent rendering: the underlying principle and React time slicing: how Fiber, Scheduler, yielding, and resumption work.

5. Stage 3: reconciliation turns element descriptions into Fiber work

Reconciliation is the stage where React takes the latest React elements and compares them with the current tree already kept in memory.

There are two common cases:

  • mount: there is no previous Fiber tree for that subtree yet
  • update: React compares new elements against existing Fibers

The important detail is that React does not immediately mutate the DOM while reconciling.

Instead, it creates or updates Fiber nodes and records what should happen later.

6. What a Fiber node adds that a React element does not

A Fiber node is much heavier than a React element because it stores the information React needs to actually do work.

A simplified mental model looks like this:

const fiber = {
  tag: HostComponent,
  type: "div",
  key: null,
  stateNode: null,
  return: parentFiber,
  child: childFiber,
  sibling: siblingFiber,
  pendingProps: { className: "app" },
  memoizedProps: { className: "old-app" },
  memoizedState: null,
  alternate: currentFiber,
  flags: Placement | Update,
};

Several fields here are what make Fiber powerful:

  • child, sibling, and return let React traverse the tree as resumable units of work
  • pendingProps and memoizedProps help React compare old and new inputs
  • memoizedState stores Hook or class state
  • stateNode eventually points to the real DOM node for host fibers
  • flags record side effects such as insert, update, or delete
  • alternate links the current tree with the work-in-progress version

This is why saying "React diffing happens on the virtual DOM" is often too vague.

The more accurate explanation is:

React compares new element output against the current Fiber tree, then builds a work-in-progress Fiber tree that records the next required mutations.

7. What "marking work" really means

During reconciliation, React decides whether a node should be:

  • inserted
  • updated
  • deleted
  • left alone

Instead of mutating the DOM immediately, it records those decisions as flags on Fiber nodes.

In simplified terms:

  • same type, changed props -> mark an update
  • new node appears -> mark placement
  • old node disappears -> mark deletion

The exact internal flag names vary across React versions, but the mental model is stable:

reconciliation produces a to-do list on the Fiber tree, not visible DOM mutations.

By the end of this stage, React has a work-in-progress tree that represents the next possible UI plus the side effects required to make it real.

8. Mount vs update: the simplest way to visualize reconciliation

Here is a helpful way to picture the difference.

On the first render

React receives React elements, creates matching Fiber nodes, and most host fibers will eventually need placement because nothing exists in the DOM yet.

On an update

React walks the current Fiber tree and the new element output together:

  • reusable nodes can be updated in place
  • unchanged subtrees can sometimes be skipped
  • removed nodes get deletion work
  • inserted nodes get placement work

So the output of reconciliation is not "the page changed." The output is:

React now knows what should change, where it should change, and with what priority.

That distinction becomes even more important once you start learning batching, lanes, and concurrent rendering. The commit cannot be correct until reconciliation has first built the right Fiber work.

9. Stage 4: commit is where React DOM finally touches the browser

Once the work-in-progress tree is complete, React enters the commit phase.

This is where the renderer, such as react-dom, applies the recorded effects to the host environment.

For the browser, that means operations such as:

  • creating elements with document.createElement(...)
  • creating text nodes
  • setting attributes, properties, and styles
  • inserting nodes into parent containers
  • removing nodes that no longer belong

In simplified form, a host fiber marked for placement eventually leads to work like this:

const domNode = document.createElement("div");
domNode.className = "app";
parentNode.appendChild(domNode);

A host fiber marked for update might instead lead to targeted prop changes:

domNode.className = "app updated";

And a deletion effect eventually removes the old host node from its parent:

parentNode.removeChild(domNode);

This is the first point in the pipeline where the browser-visible UI is actually mutated.

10. Why commit is different from render and reconciliation

The render side of React can often be paused, resumed, or restarted because it is mainly computing the next tree.

The commit side is different because it applies real mutations and must keep the visible UI consistent.

That is why a good high-level mental model is:

  • render and reconciliation calculate
  • commit mutates

And that is also why many React timing questions eventually reduce to this:

Did this happen during the render/reconciliation side, or after React entered commit?

If you want the next layer after this article, see useEffect vs useLayoutEffect: the real difference in React's commit phase.

11. A complete example from JSX to DOM

Let us connect the whole pipeline with one tiny example:

function App() {
  return (
    <div className="app">
      <h1>Hello</h1>
    </div>
  );
}

Step 1: compile

The compiler rewrites the JSX into helper calls.

Step 2: runtime

Running those helpers produces React element objects that describe a div with an h1 child.

Step 3: reconciliation

React converts that description into host Fibers, links them into the work-in-progress tree, and marks placement effects because this is a mount.

Step 4: commit

react-dom creates the real div and h1, assigns their props, inserts them into the container, and the browser can display the result.

The data shape changes at each step:

JSX
-> compiled function calls
-> React elements
-> Fiber work tree with flags
-> real DOM nodes

That is the core transformation pipeline.

12. Common misconceptions that make React internals harder to learn

"JSX is what React runs"

Not directly. JSX is compile-time syntax sugar.

"React.createElement creates DOM nodes"

No. It creates React element objects.

"Virtual DOM and Fiber are the same thing"

Not really. React elements are lightweight UI descriptions, while Fiber is the mutable work structure React uses for scheduling, diffing, and commit bookkeeping.

"Reconciliation means React already changed the page"

No. Reconciliation decides what should change. Commit is where React DOM actually mutates the page.

"Commit is the same as browser paint"

Not exactly. Commit means React has applied host mutations. The browser paint is a separate browser concern that happens around that work.

13. How to answer this in an interview

If someone asks "How does React turn JSX into the DOM?", a stronger answer is:

  1. JSX is first compiled into React.createElement or jsx helper calls
  2. Running those calls creates React element objects
  3. React reconciles those elements against the current Fiber tree and marks side effects on a work-in-progress Fiber tree
  4. The renderer uses those Fiber flags during commit to create, update, insert, or delete real DOM nodes

That answer is much better than stopping at "virtual DOM becomes real DOM" because it shows you understand the distinct roles of compilation, description objects, working data structures, and host mutations.

14. Final takeaway

From the bottom-up data structure view, React rendering is really a sequence of transformations:

  • JSX becomes JavaScript calls
  • JavaScript calls become React elements
  • React elements become Fiber work
  • Fiber work becomes DOM mutations during commit

Once that clicks, many other React internals topics become easier to place. setState, Hooks, concurrent rendering, time slicing, and effect timing are all variations on the same larger pipeline: React first computes the next UI in memory, then commits the final host changes at the right time.

Reviewed by

DevDepth Editor

Editor and frontend engineering writer

DevDepth publishes practical guides on React, Next.js, TypeScript, frontend architecture, browser APIs, and performance optimization.

Each article should be reviewed for technical accuracy, code clarity, metadata quality, and internal-link fit before it goes live.

Last editorial review: 2026-03-17

Contact the editor