Under the Hood
Reactinternals

Elements, components, and JSX

The tree React reconciles is made of elements — plain, inert objects produced by a compiler step — and getting precise about that removes most of what feels like magic in JSX and components.

Elements, components, and JSX

The previous lesson said the virtual DOM is "a tree of plain JavaScript objects" without saying exactly what those objects are. This lesson closes that gap. It also kills a common misconception head-on: JSX is not HTML, and it is not special runtime behavior. It's sugar — a compile step — for creating one specific kind of object: the React element. Once you can see through the sugar to the object underneath, components stop looking like a templating language and start looking like what they are: plain functions.

JSX is not HTML — it's a function call in disguise

Write this in a component:

const greeting = <div className="banner">hi</div>;

That is not HTML being interpreted by React at runtime. Before your code ever runs in a browser, a compiler — Babel or SWC, wired into your bundler — rewrites that JSX into a plain function call. Classically, it desugars to:

const greeting = React.createElement('div', { className: 'banner' }, 'hi');

(Modern tooling with the "automatic" JSX runtime instead emits a call to a jsx function imported from react/jsx-runtime, but the shape of the result is identical — the difference is only which function does the constructing.) By the time your code executes, there is no JSX left anywhere. <div className="banner">hi</div> and React.createElement('div', { className: 'banner' }, 'hi') are the same program, just spelled two different ways — one convenient for humans, one that JavaScript can actually run.

What createElement actually returns

Here's the part that matters most: React.createElement(...) does not create a DOM node, does not touch the screen, and does not render anything. It returns a plain JavaScript object — a React element — roughly shaped like this:

{
  type: 'div',
  props: { className: 'banner', children: 'hi' },
  key: null,
  ref: null,
}

That's it. No hidden behavior, no class instance with methods, no wrapper around a DOM node. It's a descriptor — inert data saying "at this spot in the tree, there should be a div with this class and this text inside it." Nothing has happened yet. The element hasn't been "rendered" in any sense that touches the screen; it has merely been created, the same way { x: 1, y: 2 } describes a point without drawing one.

This is why the mental model from the last lesson — "React elements are cheap" — is not a hand-wave. Creating an element is exactly as cheap as creating any small object literal, because that is literally what it is.

type is the whole trick: string vs function

Look again at the shape: { type, props, key, ref }. The entire distinction between "a DOM tag" and "a component" lives in what type holds:

  • If type is a string ('div', 'span', 'input'), the element describes a host element — a real DOM node React knows how to construct directly. There is no more unwrapping to do; React can eventually turn this into an actual document.createElement('div').
  • If type is a function (or a class), the element describes a component. React does not know how to turn a component element into DOM directly — it doesn't know what a Greeting is made of yet. So it does the only sensible thing: it calls the function, passing props, and asks it "what should this actually render as?"
function Greeting({ name }) {
  return <div className="banner">hi {name}</div>;
}

const el = <Greeting name="Ada" />;
// el is { type: Greeting, props: { name: 'Ada' }, key: null, ref: null }
// Greeting has NOT been called yet — el just names it as the type.

Notice el here does not contain a div anywhere. It contains a reference to the Greeting function itself as type, plus the props it was given. Nothing runs until React decides to render this element — at which point React calls Greeting({ name: 'Ada' }), gets back another element (the one describing the div), and repeats the process on that. This is why rendering is recursive: React keeps calling component-type elements' functions and unwrapping the result until every branch of the tree bottoms out at string-type (host) elements — the only kind it can hand off to the DOM.

That recursive unwrapping — component element → call the function → more elements → repeat until host elements — is the process the previous lesson called "producing the virtual tree." Nothing was left out; it was just described one level higher.

Components: functions of props that return elements

Strip away JSX and hooks and everything else, and a component is one thing: a function that takes props and returns elements. Not a special kind of object, not something React instantiates behind your back into some heavyweight thing — a call, same as any other function call in the language, that happens to return element objects instead of numbers or strings.

function ProductCard({ title, price, children }) {
  return (
    <div className="card">
      <h3>{title}</h3>
      <p>{price}</p>
      {children}
    </div>
  );
}

props here is the function's single argument — an object — and from the component's perspective it is immutable: ProductCard never reaches back and mutates title or price. It reads them and returns a description built from them. If the parent wants different output, it calls ProductCard again with different props; the component itself never edits its own input. children — whatever was nested between the opening and closing tags where this component was used — is not special syntax at all. It arrives as props.children, exactly one more property on the same props object, which is why you can destructure it right alongside title and price.

Elements are cheap and immutable — because they're just data

Once you see the element as a plain object, two claims from the previous lesson stop being assertions and become obvious consequences:

Cheap. Allocating { type: 'div', props: {...}, key: null, ref: null } costs whatever allocating any small object costs — nothing close to touching the DOM, running layout, or painting. Every re-render creates a fresh batch of these objects for the whole component subtree, and that is fine, because "a batch of object literals" is not expensive.

Immutable. React never takes an existing element object and mutates its props in place to reflect new state. It creates a new element object describing the new state and hands both the new tree and the old tree to the diffing process from the previous lesson. You don't edit the blueprint; you draw a new one and compare it to the old one. This is precisely why elements are safe to pass around, cache references to, and compare — nothing downstream can secretly change one out from under you.

key and ref: the two reserved props

The element shape has two slots that are not part of props: key and ref. React treats them as special because they carry information about identity and imperative access rather than rendering data:

  • ref lets you ask React for a handle to the underlying DOM node (or component instance) once it actually exists — an escape hatch out of the declarative model, for the rare cases you need to call an imperative DOM method directly.
  • key tells React which element in a list corresponds to which element in the previous list, across renders — identity, not position. It looks like an ordinary prop when you write it (<Row key={id} />), but React strips it out of props and stores it on the element separately, because it's consumed by the diffing algorithm itself, not by your component. The next lesson is entirely about why that distinction is load-bearing.

The element tree is the blueprint, not the building

It's worth restating the previous lesson's core image now that you can see the objects: the element tree is a nested structure of these { type, props, key, ref } records — a blueprint describing what should exist. It is not the real DOM. A div element is not a div node; it is a note that says "there should be a div node here, with these attributes." Turning the blueprint into an actual building — real DOM nodes, actually inserted, actually painted — is the job of reconciliation and the commit phase covered later in this module. Nothing in this lesson touches the screen; everything in this lesson is just describing it.

Go deeper

Where this goes next

You now know exactly what's in the tree the previous lesson described: element objects, chained together by component functions calling each other and returning more elements, bottoming out at host elements. The diffing algorithm picks up from here — how React compares two of these trees cheaply, the two heuristics that make it linear instead of prohibitively expensive, and exactly why key (the reserved prop from this lesson) is the thing that makes list diffing correct.

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. What does a JSX expression compile to, and when does that compilation happen — build time or runtime?
  2. Describe the shape of a React element object. Which fields does it have, and what does each one mean?
  3. Why does creating an element not put anything on the screen? What has and hasn't happened at that point?
  4. What determines whether an element is a 'host' element or a 'component' element? What does React do differently for each?
  5. Walk through what happens when React encounters a component-type element: what gets called, with what argument, and what comes back?
  6. Why are `key` and `ref` treated differently from ordinary props?
  7. Why does 'an element is just a plain, immutable object' explain why elements are cheap to recreate on every render?