Must knowReact Fundamentals · 02

Raw React APIs

createElement returns a plain, frozen object; createRoot().render() turns it into DOM later. React vs ReactDOM, and why key isn’t a prop.

On this page · 8 sections
  1. In one minute
  2. The example: the card, with React
  3. How it works
  4. key (and ref) are not props
  5. Common mistakes
  6. Interview Q&A
  7. Related
  8. Sources

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.

card.js (no JSX)
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 DOM
What you see
Objects: –
#root on the page: (empty)
  1. const h2 = createElement('h2', null, 'Ceramic Mug')createElement returns a plain object that describes a heading, not a DOM node.
  2. const p = createElement('p', { className: 'price' }, '$18.00')Another plain object for the price.
  3. 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).
  4. schedulecreateRoot(root).render(article)render() only schedules the work. Right after it returns, #root is still empty (recorded: right after render(): "").
  5. commit phase// …a moment later, React updates the DOMReact turns the objects into DOM nodes and appends them in one go.

0 / 0
createElement only builds a description (a plain object). React creates the DOM later.

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

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

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.

Sources

Primary source: react.dev/reference/react/createElement