Hone logo
Hone
Problems

Building Your Own Virtual DOM in TypeScript

React's power lies in its efficient DOM manipulation, largely thanks to its Virtual DOM implementation. This challenge asks you to build a simplified Virtual DOM from scratch in TypeScript. Understanding this process will deepen your comprehension of how modern JavaScript frameworks manage UI updates, leading to more optimized and performant applications.

Problem Description

Your task is to implement a basic Virtual DOM (VDOM) system. This system will represent your UI as a tree of JavaScript objects, allowing for efficient comparison with previous VDOM trees to determine the minimal set of DOM changes needed.

Key Requirements:

  1. VDOM Node Representation: Define a TypeScript interface or type for your VDOM nodes. This node should capture:

    • type: The HTML tag name (e.g., "div", "span", "h1").
    • props: An object containing attributes and event handlers.
    • children: An array of VDOM nodes (can be strings or other VDOM nodes).
  2. createElement Function: Implement a function createElement(type: string, props: object | null, ...children: Array<string | VDOMNode>): VDOMNode. This function will be analogous to React.createElement and will construct your VDOM node objects.

  3. render Function: Implement a function render(vnode: VDOMNode, container: HTMLElement): void. This function will take a VDOM node and an actual DOM element (the container) and mount the VDOM representation into the DOM.

  4. patch Function: Implement a function patch(parent: HTMLElement, oldVNode: VDOMNode | null, newVNode: VDOMNode | null): void. This is the core of the VDOM. It will compare two VDOM trees (oldVNode and newVNode) and apply the necessary changes to the parent DOM element to make it match newVNode.

Expected Behavior:

  • createElement should correctly construct the VDOM node structure.
  • render should translate a VDOM tree into actual DOM elements and append them to the container.
  • patch should handle various scenarios:
    • Node Addition: When a new node is added to the VDOM tree, it should be created and appended to the DOM.
    • Node Removal: When a node is removed from the VDOM tree, it should be removed from the DOM.
    • Node Replacement: If a node's type or key (if implemented) changes significantly, it should be replaced entirely.
    • Prop Updates: When props change (attributes, styles, event listeners), they should be updated on the corresponding DOM element.
    • Text Node Updates: If a text node's content changes, it should be updated.
    • Children Reordering: When the order of children changes, they should be reordered in the DOM.

Edge Cases to Consider:

  • Empty props or children.
  • Nodes with only text content.
  • Event listeners being added or removed.
  • null or undefined VDOM nodes passed to patch.

Examples

Example 1: Initial Rendering

// VDOM Node definition (assume this is provided or you define it)
interface VDOMNode {
  type: string;
  props: { [key: string]: any } | null;
  children: Array<VDOMNode | string>;
}

// Assume createElement is implemented
declare function createElement(type: string, props: object | null, ...children: Array<string | VDOMNode>): VDOMNode;

// Assume render is implemented
declare function render(vnode: VDOMNode, container: HTMLElement): void;

const appVNode = createElement(
  'div',
  { id: 'app' },
  createElement('h1', null, 'Hello, Virtual DOM!'),
  createElement('p', null, 'This is a paragraph.')
);

const container = document.getElementById('root')!;
render(appVNode, container);

Expected DOM Output:

<div id="app">
  <h1>Hello, Virtual DOM!</h1>
  <p>This is a paragraph.</p>
</div>

Explanation: The render function takes the appVNode and creates the corresponding div, h1, and p DOM elements, appending them to the root element.

Example 2: Patching - Prop Update and Text Change

// ... (previous setup)

const oldAppVNode = createElement(
  'div',
  { id: 'app', className: 'container' },
  createElement('h1', null, 'Hello, Virtual DOM!')
);

const newAppVNode = createElement(
  'div',
  { id: 'app', className: 'wrapper' }, // className changed
  createElement('h1', null, 'Hello, Updated DOM!') // Text content changed
);

const container = document.getElementById('root')!;
render(oldAppVNode, container); // Initial render
patch(container, oldAppVNode, newAppVNode); // Patching

Expected DOM Output after patching:

<div id="app" class="wrapper">
  <h1>Hello, Updated DOM!</h1>
</div>

Explanation: The patch function detects that the className prop on the div has changed and updates it. It also detects that the text content of the h1 has changed and updates it.

Example 3: Patching - Child Addition and Removal

// ... (previous setup)

const oldRootVNode = createElement(
  'div',
  null,
  createElement('span', null, 'First Item')
);

const newRootVNode = createElement(
  'div',
  null,
  createElement('span', null, 'First Item'),
  createElement('p', null, 'New Item') // New child added
);

const container = document.getElementById('root')!;
render(oldRootVNode, container);
patch(container.children[0] as HTMLElement, oldRootVNode.children[0] as VDOMNode, newRootVNode.children[0] as VDOMNode); // This example assumes patch is applied to the parent's direct children

// More realistically, patch is called on the container itself
patch(container, oldRootVNode, newRootVNode);

Expected DOM Output after patching:

<div>
  <span>First Item</span>
  <p>New Item</p>
</div>

Explanation: The patch function identifies that a new p element has been added as a child and appends it to the DOM.

Constraints

  • Implement the VDOM logic purely in TypeScript.
  • Do not use any existing VDOM libraries or React.createElement.
  • Focus on the core diffing and patching algorithm. Handling complex scenarios like component lifecycles, state management, or reconciliation for lists with keys is out of scope for this challenge.
  • The patch function should be reasonably efficient, meaning it avoids unnecessary DOM manipulations.

Notes

  • Think about how to represent different types of nodes (element nodes, text nodes).
  • Consider how to handle event listeners. You might need to attach and detach them carefully during patching.
  • The props object can contain attributes like className, id, style, and event handlers like onClick.
  • For simplicity in this challenge, you don't need to implement a key-based reconciliation for lists, though it's a crucial optimization in real-world VDOMs. Focus on correctly identifying additions, removals, and replacements of children.
  • The patch function will likely be recursive.
Loading editor...
typescript