InterviewsVector
advancedRare5 min read · Updated Jul 18, 2026

Virtual DOM II: Render the Object Tree Back to Real DOM

Deserialize a virtual DOM tree into real DOM nodes in JavaScript — createElement recursion, SVG namespaces, setAttribute vs properties, and the XSS question.


The problem

Write deserialize(vNode): turn the { type, props, children } tree from part one back into real DOM nodes — a lossless round trip.

This is the render half of the pipeline — what React's ReactDOM does on mount, minus reconciliation. The recursion is easy; the interview lives in three specifics: setAttribute vs properties, SVG namespaces, and why this approach is XSS-safe where innerHTML isn't.

Implementation

function deserialize(vNode) {
  if (vNode.type === "text") {
    // createTextNode does NO HTML parsing — this line is the XSS answer
    return document.createTextNode(vNode.props.nodeValue);
  }
 
  const element = document.createElement(vNode.type);
 
  for (const [name, value] of Object.entries(vNode.props)) {
    element.setAttribute(name, value);
  }
 
  for (const child of vNode.children) {
    element.appendChild(deserialize(child));
  }
 
  return element;
}
 
// Mount:
document.getElementById("root").appendChild(deserialize(vdom));

Round trip verified

const vdom = {
  type: "div", props: { id: "app" }, children: [
    { type: "h1", props: { class: "title" }, children: [
      { type: "text", props: { nodeValue: "Hello!" }, children: [] },
    ]},
  ],
};
 
const dom = deserialize(vdom);
dom.outerHTML; // '<div id="app"><h1 class="title">Hello!</h1></div>'
serialize(dom); // deep-equals the input vdom — lossless ✓

The three specifics that carry the question

1. setAttribute vs properties — the layer distinction, again. setAttribute("class", ...) writes markup-layer attributes; but some things only work as properties: input.value (the live value), checked, el.onclick. And event handlers can't be attributes at all in this design — a serialized tree stores data, not functions (part one explains why they were never captured). React's DOM layer maintains an explicit per-prop policy — property here, attribute there, event-listener registry for on* — and "props need a dispatch table, not one code path" is the senior observation. For this exercise's attribute-shaped input, setAttribute is correct; knowing where it stops being correct is the credit.

2. SVG needs a namespace. document.createElement("svg") creates an HTMLUnknownElement that renders nothing — SVG elements must come from createElementNS("http://www.w3.org/2000/svg", type). The fix threads a namespace through the recursion (enter SVG mode at <svg>, stay in it for descendants). Every real VDOM library has exactly this code; interviewers use SVG as the "did you only ever render divs?" probe:

const SVG_NS = "http://www.w3.org/2000/svg";
function deserialize(vNode, ns = null) {
  if (vNode.type === "text") return document.createTextNode(vNode.props.nodeValue);
  const childNs = vNode.type === "svg" ? SVG_NS : ns;
  const element = childNs
    ? document.createElementNS(childNs, vNode.type)
    : document.createElement(vNode.type);
  // ...attributes as before...
  for (const child of vNode.children) {
    element.appendChild(deserialize(child, childNs));
  }
  return element;
}

3. Why this is XSS-safe — and where it isn't. Text becomes createTextNode, which never parses HTML: a nodeValue of "<img onerror=...>" renders as those literal characters. That's the structural safety innerHTML lacks, and it's why frameworks are safe by default. The honest caveats that complete the answer: attribute values can still be dangerous for URL-taking attributes (href="javascript:...") and inline handlers (onclick="..." set via setAttribute does create live behavior) — so a hardened renderer allow-lists attributes. Safe-by-construction for content, policy-needed for attributes: that two-part sentence is the security credit in this question.

Performance: the batching detail

Each appendChild above touches the live document only at the very end — the subtree is built detached, then mounted once. That's already the batching win (one layout invalidation, not N). If asked about many siblings: DocumentFragment is the classic container for the same idea. And the deeper framing interviewers fish for: build-detached-then-swap is the mount optimization; diffing (the missing part three) is the update optimization — this function rebuilds everything, which is exactly what reconciliation exists to avoid.

Edge cases interviewers probe

  • Unknown/invalid typescreateElement("not-a-tag") happily creates an unknown element (and hyphenated names collide with custom elements); validating type against an allow-list is the hardened version.
  • Boolean attributessetAttribute("disabled", "") correctly disables; but setAttribute("disabled", "false") also disables (presence is truth). Data from part one round-trips fine; hand-written vNodes with disabled: false are the trap — another vote for the per-prop policy table.
  • style as an object? — this exercise's props are strings; React accepts style objects and writes element.style[k] per key. If your input format allows objects, you need that branch — clarify the contract before coding.
  • Text node adjacency — two adjacent text vNodes produce two DOM text nodes; normalize() merges them; rendering is identical. Trivia-grade, but it explains surprising childNodes.lengths.
  • Re-entrancy with the document — building while detached means no reflow per node and no visible half-built UI; mounting into a live tree mid-build is the perf/UX bug to name.

Common mistakes

  • innerHTML-based "rendering" (el.innerHTML += ...) — quadratic re-parsing and the XSS door this design exists to close.
  • No SVG namespace handling (silent blank rendering — the worst failure mode: no error).
  • Setting event handlers via setAttribute("onclick", ...) and calling it a feature.
  • Mounting each node into the live document as it's created.
  • Claiming the round trip preserves behavior — listeners were never in the data; only structure round-trips.

Follow-up questions

  • "Write part three: diff(oldV, newV) → patches."implemented here: same-type nodes recurse per-prop; different types replace subtrees; keyed lists match by key before position — React reconciliation's O(n) heuristic set in three clauses.
  • "Apply patches with minimal DOM writes?" — patch ops as data ({ op: "setAttr", path, name, value }) then a tiny interpreter — the pipe-shaped architecture of every VDOM library's commit phase.
  • "How does server-side rendering relate?" — SSR serializes the vNode tree to an HTML string (escaping!), then the client hydrates: build the VDOM, walk the existing DOM, attach listeners without recreating nodes — deserialize's cousin that adopts instead of creates. One paragraph of this is a strong senior riff.
  • "Custom elements / web components?" — hyphenated types trigger custom element upgrade on createElement; attributes drive attributeChangedCallback — the platform's own component model meeting your renderer.
  • "When is the VDOM the wrong tool?" — fine-grained reactivity (Solid, Svelte 5 runes) compiles updates directly to DOM mutations, skipping tree-diffing entirely; text-heavy mostly-static content wants plain templates. Knowing the 2026 landscape's answer to "why not always VDOM" closes the loop the question opened.

  • Diff two virtual trees and apply minimal DOM mutations — the reconciliation step that makes the virtual DOM worth having.

  • Turn real DOM into a plain-object tree — the first half of understanding how React represents UI.

  • classnames()

    beginner

    Rebuild the classnames utility: strings, arrays, objects, and nested combinations.

  • Browser History

    intermediate

    Model back/forward/push navigation with an index and a stack — the core of every router.