Skip to content
GeeksSmith
advanced 9 min read

Building React-like Class Components from Scratch (ES5 Prototypes & Mounting)

Building React-like components in ES5 involves creating a base `Component` constructor function with `setState` and `render` on `Component.prototype`, implementing prototype inheritance via `Object.create`, and wiring a micro-reconciler to mount and re-render the DOM tree.

Why it matters in interviews

A classic Staff/Lead interview problem at Tekion Corp, Uber, and Meta. It directly tests whether you understand how JavaScript OOP works without ES6 `class` syntactic sugar, how `this` binding works, how `setState` queues updates, and how React works under the hood.

Visual & Interactive Explanation

Interactive Flow Execution

Code Examples & Implementation

Complete ES5 Component Runtime from Scratch
// --- 1. BASE COMPONENT CONSTRUCTOR ---
function Component(props) {
  this.props = props || {};
  this.state = {};
  this._dom = null;
  this._pendingState = [];
  this._isBatching = false;
}

// Attach setState to prototype
Component.prototype.setState = function(partialState) {
  this._pendingState.push(partialState);

  if (!this._isBatching) {
    this._isBatching = true;
    queueMicrotask(() => {
      this._flushState();
    });
  }
};

Component.prototype._flushState = function() {
  // Merge all queued state updates
  while (this._pendingState.length > 0) {
    const next = this._pendingState.shift();
    const resolved = typeof next === "function" ? next(this.state, this.props) : next;
    this.state = Object.assign({}, this.state, resolved);
  }
  this._isBatching = false;
  this._update();
};

Component.prototype._update = function() {
  if (!this._dom || !this._dom.parentNode) return;
  const newVNode = this.render();
  const newDom = mount(newVNode);
  this._dom.parentNode.replaceChild(newDom, this._dom);
  this._dom = newDom;
};

// --- 2. VDOM & MOUNTING PIPELINE ---
function createElement(type, props, ...children) {
  return {
    type: type,
    props: Object.assign({}, props, {
      children: children.flat().filter((c) => c !== null && c !== undefined && c !== false),
    }),
  };
}

function mount(vnode, container) {
  // Text node
  if (typeof vnode === "string" || typeof vnode === "number") {
    const textNode = document.createTextNode(String(vnode));
    if (container) container.appendChild(textNode);
    return textNode;
  }

  // Class Component
  if (typeof vnode.type === "function") {
    const instance = new vnode.type(vnode.props);
    const renderedVNode = instance.render();
    const dom = mount(renderedVNode);
    instance._dom = dom;
    if (instance.componentDidMount) instance.componentDidMount();
    if (container) container.appendChild(dom);
    return dom;
  }

  // HTML Element (div, button, h1)
  const dom = document.createElement(vnode.type);

  // Attach props & event listeners
  if (vnode.props) {
    Object.keys(vnode.props).forEach((key) => {
      if (key === "children") return;
      if (key.startsWith("on")) {
        const eventName = key.slice(2).toLowerCase();
        dom.addEventListener(eventName, vnode.props[key]);
      } else {
        dom.setAttribute(key, vnode.props[key]);
      }
    });

    if (vnode.props.children) {
      vnode.props.children.forEach((child) => mount(child, dom));
    }
  }

  if (container) container.appendChild(dom);
  return dom;
}
Writing an ES5 Custom Component using Prototype Chaining
// --- 3. CUSTOM USER COMPONENT IN PURE ES5 ---
function CounterComponent(props) {
  // Call super constructor
  Component.call(this, props);
  this.state = { count: 0 };
}

// Inherit from Component.prototype
CounterComponent.prototype = Object.create(Component.prototype);
CounterComponent.prototype.constructor = CounterComponent;

// Implement render method
CounterComponent.prototype.render = function() {
  return createElement(
    "div",
    { class: "counter-box" },
    createElement("h1", null, "Count: " + this.state.count),
    createElement(
      "button",
      {
        onClick: () => {
          this.setState({ count: this.state.count + 1 });
        },
      },
      "Increment"
    )
  );
};

// Mount into DOM
// mount(createElement(CounterComponent, null), document.getElementById("root"));

Interview-Ready Answers

Create a `Component` constructor function, attach `setState` to its prototype to merge partial state and schedule a re-render, inherit using `Object.create(Component.prototype)`, and write a `mount` function to convert VNodes to real DOM nodes.

Follow-up Questions & Deep Dives

🎯 Code Output — What Does This Print?

Question 1hard
function Parent() {}
function Child() {}

Child.prototype = Object.create(Parent.prototype);

console.log(Child.prototype.constructor === Parent);
console.log(new Child() instanceof Parent);

Common Mistakes & Anti-Patterns

  • Mutating `this.state` directly instead of merging in `setState`
  • Forgetting to flat/filter children arrays when creating VNodes
  • Not binding event listener callbacks, causing `this` to point to the DOM element instead of the component instance

Implementation Evolution: Anti-Pattern to Production

❌ Anti-Pattern: Direct Prototype Assignment (`Child.prototype = Parent.prototype`)

// ❌ Fatal Anti-Pattern: Mutating the base prototype directly
function MyComponent() {}

// Broken: This points to the EXACT SAME object in memory!
MyComponent.prototype = Component.prototype;

// Adding methods to MyComponent now pollutes ALL components in the entire app!
MyComponent.prototype.customMethod = function() {};
Why this breaks: Directly assigning `Child.prototype = Parent.prototype` does not create an inheritance prototype chain. Any method added to `MyComponent.prototype` directly mutates `Component.prototype`, breaking all other components.

✅ Correct Pattern: Clean Prototype Inheritance with `Object.create`

// ✅ Correct: Creates a fresh prototype object linked to Component.prototype
function MyComponent(props) {
  Component.call(this, props);
}

MyComponent.prototype = Object.create(Component.prototype);
MyComponent.prototype.constructor = MyComponent; // Restore constructor pointer!
🚀

🚀 Production-Grade (Hardened): ES5 Component Helper with Lifecycle & Shallow State Merge

// 🚀 Production: Factory helper similar to React.createClass (ES5 Standard)
function createClass(spec) {
  function Constructor(props) {
    Component.call(this, props);
    if (spec.getInitialState) {
      this.state = spec.getInitialState.call(this);
    }
  }

  Constructor.prototype = Object.create(Component.prototype);
  Constructor.prototype.constructor = Constructor;

  // Copy spec methods to prototype and bind auto-methods
  Object.keys(spec).forEach(function(key) {
    if (key !== "getInitialState") {
      Constructor.prototype[key] = spec[key];
    }
  });

  return Constructor;
}

⚠️ Trick Questions & Interviewer Traps

Q1:Why must you reset `Child.prototype.constructor = Child` after `Object.create`?

Q2:Why do we call `Component.call(this, props)` inside the subclass constructor?

📋 Rapid Revision Cheat Sheet

  • Base Constructor: `function Component(props) { this.props = props; this.state = {}; }`.
  • Inheritance: `Sub.prototype = Object.create(Component.prototype); Sub.prototype.constructor = Sub;`.
  • Super Call: `Component.call(this, props)` initializes instance fields.
  • setState: Merges partial state and schedules microtask `_update()` flush.
  • Mount: Inspects `typeof vnode.type === 'function'` to instantiate class and call `.render()`.

Real-World Architectural Scenario

An interviewer asks you to build React without using ES6 classes or JSX. How do you implement `setState` and component rendering in 30 lines of code?

Rate Your Readiness

Rate how comfortably you can explain this in an interview to update your global readiness gauge:

Rate your confidence: