Elements Are Just Objects
What you'll learn
- What actually gets created when you write JSX
- Why React elements are descriptions of UI, not UI itself
- The exact shape of an element object
- Why elements being cheap, immutable plain objects is the foundation of everything React does
When developers first learn React, they often imagine that writing <div>Hello</div> somehow creates a div, or tells React to "manage" a div. Nothing like that happens. Writing JSX is closer to writing a shopping list than cooking the meal. Understanding this one fact, really internalizing it, makes the rest of React's design feel inevitable instead of magical.
The big idea: UI as data
Here is the entire trick that React is built on:
What if we could describe what the UI should look like, as plain data, and let a machine figure out how to make the real UI match that description?
That "plain data" is the element.
Jargon: "element". A plain JavaScript object that describes one piece of UI: what type it is (
'div', a component…), what props it has, and what its children are. It is not a DOM node. It costs almost nothing to create.
What JSX really produces
When you write this:
function Greeting() {
return (
<div className="card">
<h1>Hello</h1>
<p>Welcome back</p>
</div>
);
}
Your build tool compiles the JSX into ordinary function calls. Conceptually it becomes:
function Greeting() {
return jsx('div', {
className: 'card',
children: [
jsx('h1', { children: 'Hello' }),
jsx('p', { children: 'Welcome back' }),
],
});
}
Jargon: "JSX". A syntax extension that lets you write HTML-looking markup inside JavaScript. Browsers can't run it; a compiler (Babel, esbuild, SWC…) converts it to function calls before your code ever runs.
And those jsx(...) calls don't do anything smart. Each one just assembles a small object and returns it. The result of rendering <Greeting /> is roughly:
{
type: 'div',
key: null,
props: {
className: 'card',
children: [
{ type: 'h1', key: null, props: { children: 'Hello' } },
{ type: 'p', key: null, props: { children: 'Welcome back' } },
],
},
}
That is the whole mystery. A React element is a frozen, plain object with a type, a key, and props. (There is also a special marker field so React can recognize its own elements, and in development mode a few extra debugging fields, but conceptually: type, key, props.)
What happens:
- You write JSX in a component.
- The build step turns JSX into
jsx()calls. - When the component function runs, those calls execute and return plain nested objects.
- React receives those objects and uses them as instructions.
Step 3 is crucial: creating elements is just creating objects. No DOM is touched. No browser API is called. You could create elements in Node.js, in a test, on a server, anywhere JavaScript runs.
You can print an element
Because elements are plain objects, you can inspect them yourself:
function App() {
const ui = <button onClick={() => alert('hi')}>Save</button>;
console.log(ui);
return ui;
}
What happens: your console shows an object. You'll see type: 'button', props: { onClick: [Function], children: 'Save' }, and key: null. Notice the event handler is just data sitting in props, nothing has been attached to any DOM node yet.
This is why the same component can be rendered by different "machines": the DOM renderer turns elements into browser nodes, the test renderer turns them into plain JSON, a native renderer turns them into native views. The element is renderer-agnostic description data.
Jargon: "renderer". The machine that turns element descriptions into something real.
react-domis the renderer you know; others exist (test, native, ART). React's brain is separate from any renderer's hands.
Elements are cheap: and that's the point
A common early worry is: "React re-creates all these objects on every render, isn't that wasteful?" It's a fair question, and the answer is a deliberate engineering trade:
- Creating a few hundred small objects is extremely fast, microseconds.
- Touching the real DOM is slow, style recalculation, layout, paint.
- So React optimizes for: do lots of cheap object work in JavaScript to minimize expensive DOM work.
The entire architecture (the diffing you'll learn about in Part 2) exists to make this trade pay off: recreate the description freely, then compute the smallest possible set of real changes.
Pseudocode model, not real source:
// What jsx() conceptually does:function jsx(type, config) {const { key = null, ...props } = config;return { type, key, props }; // plus a marker so React recognizes it}
Elements are immutable
In development builds, React freezes elements and their props (Object.freeze). If you try to mutate one:
const ui = <div className="a">hi</div>;
ui.props.className = 'b'; // TypeError in development (frozen object)
What happens: in development you get an error; in production it silently does nothing useful. Either way, you must never mutate elements.
Why freeze? Because elements are shared descriptions. React may hold onto an element object, compare it with another, pass it between internal trees. If your code could mutate an element after handing it over, all those comparisons become meaningless. Immutability is what lets React treat elements as reliable snapshots of "what the UI should be" at a moment in time.
The render = function call mental model
Putting it together, "rendering" a component means exactly one thing:
Calling your function and collecting the element objects it returns.
function Profile({ name, online }) {
return (
<div>
<Avatar status={online} />
<span>{name}</span>
</div>
);
}
When React renders <Profile name="Ada" online={true} />, it calls Profile({ name: 'Ada', online: true }) and gets back an element tree. That tree might contain other component elements (<Avatar status={true} />), which React then renders the same way, calling Avatar(...), until the whole tree is made of host elements.
Jargon: "host element". An element whose type is a string naming a platform primitive (
'div','span','button'). These are the leaves of the tree; the renderer knows how to create real instances for them. Component elements (type is a function) are just waypoints that must be "unwrapped" by calling the function.
Diagram
Rendered diagram (PNG hi-res):
Rendered diagram (PNG hi-res):
Common misconceptions
- Misconception: JSX creates DOM elements. Reality: JSX creates plain JavaScript objects. DOM nodes appear much later, created by the renderer during a separate phase.
- Misconception: A component "returns HTML". Reality: a component returns element objects. Nothing HTML-ish exists until the renderer builds it.
- Misconception: Re-rendering recreates the page. Reality: re-rendering recreates descriptions; React then figures out the minimal real changes (usually tiny or zero).
- Misconception: Elements are tied to the browser. Reality: elements are platform-neutral data, the same element tree can drive DOM, native mobile views, or a test snapshot.
- Misconception:
props.childrenis special magic. Reality: children are just a prop, usually an element, an array of elements, a string, or a number, passed inside the props object like everything else.
Why it works this way
- Descriptions decouple intent from execution. You say what the UI should be; React decides how and when to make it real. That separation is what later enables time-slicing, server rendering, and non-DOM targets.
- Plain objects are comparable. Because UI can be represented as data, React can diff "what you asked for now" vs "what you asked for before" with ordinary JavaScript, that diff is the heart of the engine (Part 2).
- Cheap to recreate = simple programming model. You never mutate UI objects; you just describe the new state of the world and let React reconcile. No manual bookkeeping of "which DOM nodes need updating".
- Frozen in dev = bugs surface early. Mutating shared descriptions is always a bug; freezing turns it into a loud error instead of a silent inconsistency.
Try it yourself
- In any React app, add
console.log(<div id="x">hello</div>)inside a component. Open DevTools and expand the object. Findtype,key, andprops.children. - Write a component that stores JSX in a variable, logs
typeof uiandArray.isArray(ui.props.children), and returns it. Predict the output before you run it. - In development mode, try
ui.props.children = 'hacked'after creating an element and observe the frozen-object error. Now you know why.
Recap
- JSX compiles to plain function calls (
jsx(type, config)). - Those calls return elements: frozen, plain objects shaped like
{ type, key, props }. - Elements describe UI; they are not UI. Creating them touches no DOM.
- "Render" = call the component function, collect the element tree.
- Host elements (string types) are the leaves that a renderer can materialize.
- The whole React trade: recreate cheap descriptions freely → compute minimal expensive DOM changes.