In one minute
React for the web is two packages. react creates descriptions of UI; react-dom turns those descriptions into real DOM. createElement(type, props, ...children) doesn’t create a DOM node: it returns a small, frozen JavaScript object, a React element. Nothing touches the page until you hand that object to createRoot(container).render(element), and even then the DOM work happens a moment later.
You’ll be able to: say what a React element is, read one, and explain why key never shows up in props.
1const h2 = createElement('h2', null, 'Ceramic Mug')2const p = createElement('p', { className: 'price' }, '$18.00')3const article = createElement('article',4 { className: 'product-card' }, h2, p)56createRoot(root).render(article)7// …a moment later, React updates the DOMconst h2 = createElement('h2', null, 'Ceramic Mug')createElementreturns a plain object that describes a heading, not a DOM node.const p = createElement('p', { className: 'price' }, '$18.00')Another plain object for the price.const article = createElement('article',The outer element gets the two objects as its children. The result describes the whole card, and it’s frozen (recorded:frozen? true true).- schedule
createRoot(root).render(article)render()only schedules the work. Right after it returns,#rootis still empty (recorded:right after render(): ""). - commit phase
// …a moment later, React updates the DOMReact turns the objects into DOM nodes and appends them in one go.
The example: the card, with React
The same product card as in Hello World in JS, described with createElement:
const element = createElement(
'article',
{ className: 'product-card' },
createElement('h2', null, product.name),
createElement('p', { className: 'price' }, formatUSD(product.priceCents)),
)
The object it returns, as logged by the lesson (nested elements included):
{
"$$typeof": "Symbol(react.transitional.element)",
"type": "article",
"key": null,
"props": {
"className": "product-card",
"children": [
{
"$$typeof": "Symbol(react.transitional.element)",
"type": "h2",
"key": null,
"props": {
"children": "Ceramic Mug"
}
},
{
"$$typeof": "Symbol(react.transitional.element)",
"type": "p",
"key": null,
"props": {
"className": "price",
"children": "$18.00"
}
}
]
}
}
Read it as “an article with a class, whose children are an h2 element and a p element”. $$typeof is a Symbol that marks the object as a genuine React element, so a plain JSON object from an API can’t be mistaken for one.
Then the lesson renders it, and checks #root twice:
const reactRoot = createRoot(root)
reactRoot.render(element)
element: {"$$typeof":"Symbol(react.transitional.element)","type":"article","key":null,"props":{"className":"product-card","children":[{"$$typeof":"Symbol(react.transitional.element)","type":"h2","key":null,"props":{"children":"Ceramic Mug"}},{"$$typeof":"Symbol(react.transitional.element)","type":"p","key":null,"props":{"className":"price","children":"$18.00"}}]}}
frozen? true true
keyed element: {"key":"p1","props":{"className":"item","children":"Ceramic Mug"}}
right after render(): ""
a moment later: <article class="product-card"><h2>Ceramic Mug</h2><p class="price">$18.00</p></article>
How it works
childrenis just a prop. Extra arguments afterpropsbecomeprops.children(an array when there are several). A string child is turned into a text node by React; you don’t wrap it.- Elements are immutable. In development React freezes both the element and its
props(frozen? true trueabove). A new render creates new elements instead of editing old ones, which is what lets React compare two renders safely. render()schedules; it doesn’t paint. Right afterrender()returns,#rootis still"". React does the DOM work shortly after, in one batch. Render and Commit explains that step.- React vs ReactDOM.
react(elements, components, hooks) knows nothing about the browser.react-domis one renderer; React Native and others reuse the same model on other platforms.
key (and ref) are not props
const item = createElement('li', { key: 'p1', className: 'item' }, 'Ceramic Mug')
{
"key": "p1",
"props": {
"className": "item",
"children": "Ceramic Mug"
}
}
React keeps key on the element itself and removes it from props, so a component can never receive it by accident and can’t use the name for something else. Keys get their own note: Rendering Arrays.
Common mistakes
- Expecting
createElementto return a DOM node. You can’t call.appendChildon it; it’s a description. - Mutating an element (
element.props.className = …). It’s frozen in development; create a new element instead. - Reading the DOM right after
render()and finding it empty. The update hasn’t been committed yet.
Interview Q&A
What does createElement return, and why does that matter?
A plain object describing what should be on screen, e.g. { $$typeof, type: 'article', key: null, props: { className: 'product-card', children: [h2, p] } }. No DOM is created. Because a render produces cheap objects, React can compare the new description with the previous one and only then decide which (expensive) DOM changes are needed.
What's the difference between React and ReactDOM?
react is platform-independent: it creates elements and runs components and hooks. react-dom is the renderer for browsers: createRoot(container).render(element) is what turns elements into DOM nodes. Other renderers (React Native, react-three-fiber, …) reuse the same core.
Are React elements mutable?
No. They’re snapshots: in development both the element and its props are frozen (the recording logged frozen? true true). Each render creates new elements; React relies on old ones never changing.
Why are key and ref treated specially?
React needs them for itself (key to match list items between renders, ref for DOM/instance access), so it takes them out of props. The recorded element for createElement('li', { key: 'p1', className: 'item' }, 'Ceramic Mug') has key: "p1" and props { className, children } only.
When does the DOM actually change after root.render(element)?
Not synchronously. In the recording #root was still empty right after render() returned and contained the card a moment later. render queues an update and schedules a render; the DOM is changed in the commit that follows.
Related
- Using JSX: the same objects, with nicer syntax.
- How React Works, Start to Finish (React Internals): when elements are created and when components run, traced through a real app.
Sources
- react.dev:
createElement,createRoot - Topic order inspired by Kent C. Dodds’ EpicReact React Fundamentals workshop; the example app and code here are this handbook’s own.
Primary source: react.dev/reference/react/createElement