from scratch

Tiny React

React’s core idea is smaller than it looks: describe your UI as plain JavaScript objects (a virtual DOM), then have a render function translate those objects into real DOM nodes. Here we build both — in about 20 lines — and watch a tree of objects become actual elements on the page.

Live demo
1 · virtual DOM (plain objects)
{
  "type": "div",
  "props": {
    "attrs": {
      "id": "tiny-root"
    },
    "children": [
      {
        "type": "h1",
        "props": {
          "attrs": {},
          "children": [
            {
              "type": "TEXT_ELEMENT",
              "props": {
                "nodeValue": "Hello Tiny React",
                "children": []
              }
            }
          ]
        }
      },
      {
        "type": "p",
        "props": {
          "attrs": {},
          "children": [
            {
              "type": "TEXT_ELEMENT",
              "props": {
                "nodeValue": "Rendered by a 20-line renderer",
                "children": []
              }
            }
          ]
        }
      }
    ]
  }
}
2 · mounted to the real DOM
↑ real <div>, <h1> and <p> nodes, created by our renderer.
Left: the virtual DOM — just nested objects with a type and props. Right: the real DOM our render function produced from it.
How it works

Step 1 — describe the UI as objects

createElement doesn’t touch the DOM at all. It just returns a plain object describing what you want: a type ("div", "h1", …), its attributes, and its children. Strings become special text nodes. This is exactly what JSX compiles into — <h1>hi</h1> becomes createElement("h1", null, "hi").

TSX
1
// A text node is just an object with a special type.
2
function createTextElement(text) {
3
  return { type: "TEXT_ELEMENT", props: { nodeValue: text, children: [] } };
4
}
5
 
6
// createElement returns a plain object — the "virtual DOM" node.
7
// This is what JSX compiles down to: <h1>hi</h1> → createElement("h1", null, "hi")
8
function createElement(type, props, ...children) {
9
  return {
10
    type,
11
    props: {
12
      attrs: props ?? {},
13
      children: children.map(child =>
14
        typeof child === "string" ? createTextElement(child) : child
15
      ),
16
    },
17
  };
18
}

Step 2 — render the objects to the real DOM

render walks that object tree recursively. For a text node it creates a real text node; for an element it creates the tag, copies attributes across, and recurses into the children — appending each result to its parent. One depth-first pass turns the whole virtual tree into live DOM.

TSX
1
// render walks the virtual tree and builds real DOM nodes.
2
function render(element, container) {
3
  let dom;
4
 
5
  if (element.type === "TEXT_ELEMENT") {
6
    dom = document.createTextNode(element.props.nodeValue);
7
  } else {
8
    dom = document.createElement(element.type);
9
    // copy attributes onto the real node
10
    Object.entries(element.props.attrs).forEach(([name, value]) =>
11
      dom.setAttribute(name, value)
12
    );
13
    // recurse into children
14
    element.props.children.forEach(child => render(child, dom));
15
  }
16
 
17
  container.appendChild(dom);
18
}
19
 
20
// Build a tree and mount it:
21
const tree = createElement(
22
  "div", { id: "tiny-root" },
23
  createElement("h1", null, "Hello Tiny React"),
24
  createElement("p", null, "Rendered by a 20-line renderer")
25
);
26
render(tree, document.getElementById("app"));
What’s missing (and why React is more)
This renderer only mounts — it builds the DOM once. Real React adds reconciliation (diffing a new virtual tree against the old one and updating only what changed), components & hooks (state, effects), and event handling. But the foundation is exactly this: UI as data, plus a function that projects that data onto the screen.