Virtual DOM III: Diff & Patch
advancedDiff two virtual trees and apply minimal DOM mutations — the reconciliation step that makes the virtual DOM worth having.
Deserialize a virtual DOM tree into real DOM nodes in JavaScript — createElement recursion, SVG namespaces, setAttribute vs properties, and the XSS question.
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.
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));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 ✓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.
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.
createElement("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.setAttribute("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.normalize() merges them; rendering is identical. Trivia-grade, but it explains surprising childNodes.lengths.innerHTML-based "rendering" (el.innerHTML += ...) — quadratic re-parsing and the XSS door this design exists to close.setAttribute("onclick", ...) and calling it a feature.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.{ op: "setAttr", path, name, value }) then a tiny interpreter — the pipe-shaped architecture of every VDOM library's commit phase.types trigger custom element upgrade on createElement; attributes drive attributeChangedCallback — the platform's own component model meeting your renderer.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.
Rebuild the classnames utility: strings, arrays, objects, and nested combinations.
Model back/forward/push navigation with an index and a stack — the core of every router.