Lesson 14-Algorithms and Frontend Comprehensive Projects

Building a Custom React-Like Library

We’re going to build Miniact (think “mini React”). It won’t have all of React’s features, but it will have the core ones:

  1. JSX support (using Miniact.createElement)
  2. Component rendering (functions that return JSX)
  3. State management (Miniact.useState)
  4. Effect system (Miniact.useEffect)
  5. Virtual DOM (with diffing)
  6. Event handling

Let’s start with the foundation: the createElement function (which Babel compiles JSX to).

The createElement Function

When you write JSX like <div className="hello">World</div>, Babel compiles it to:

Miniact.createElement('div', { className: 'hello' }, 'World');

The createElement function creates a virtual DOM node. It’s a simple data structure:

// The createElement function
function createElement(type, props, ...children) {
  return {
    type,
    props: props || {},
    children: children.map(child => 
      typeof child === 'string' ? createTextElement(child) : child
    )
  };
}

// Helper: create a text element
function createTextElement(text) {
  return {
    type: 'TEXT_ELEMENT',
    props: { nodeValue: text },
    children: []
  };
}

This is it. This is the foundation of your entire library. Every JSX element becomes a call to createElement, which returns a plain JavaScript object (the virtual DOM node).

Let’s verify this works:

// Test our createElement
const element = createElement(
  'div',
  { className: 'container' },
  createElement('h1', {}, 'Hello, Miniact!'),
  createElement('p', {}, 'This is a paragraph.')
);

console.log(JSON.stringify(element, null, 2));

Output:

{
  "type": "div",
  "props": { "className": "container" },
  "children": [
    {
      "type": "h1",
      "props": {},
      "children": [
        { "type": "TEXT_ELEMENT", "props": { "nodeValue": "Hello, Miniact!" }, "children": [] }
      ]
    },
    {
      "type": "p",
      "props": {},
      "children": [
        { "type": "TEXT_ELEMENT", "props": { "nodeValue": "This is a paragraph." }, "children": [] }
      ]
    }
  ]
}

This is your virtual DOM tree. Now you need to render it to real DOM.

Rendering to Real DOM

The render function takes a virtual DOM node and creates the corresponding real DOM node:

// Render a virtual DOM node to real DOM
function render(virtualNode, container) {
  // Case 1: Text node
  if (virtualNode.type === 'TEXT_ELEMENT') {
    const textNode = document.createTextNode(
      virtualNode.props.nodeValue
    );
    container.appendChild(textNode);
    return textNode;
  }
  
  // Case 2: Regular element
  const domNode = document.createElement(virtualNode.type);
  
  // Set props (simplified - doesn't handle events, etc.)
  updateProps(domNode, {}, virtualNode.props);
  
  // Recursively render children
  virtualNode.children.forEach(child => {
    render(child, domNode);
  });
  
  // Append to container
  container.appendChild(domNode);
  
  return domNode;
}

// Update props on a DOM node
function updateProps(domNode, oldProps, newProps) {
  // Remove old props
  Object.keys(oldProps).forEach(propName => {
    if (!(propName in newProps)) {
      setProp(domNode, propName, null, oldProps[propName]);
    }
  });
  
  // Set new/changed props
  Object.keys(newProps).forEach(propName => {
    if (oldProps[propName] !== newProps[propName]) {
      setProp(domNode, propName, newProps[propName], oldProps[propName]);
    }
  });
}

// Set a single prop on a DOM node
function setProp(domNode, propName, newValue, oldValue) {
  // Handle event handlers (onClick, etc.)
  if (propName.startsWith('on')) {
    const eventName = propName.toLowerCase().substring(2);
    if (oldValue) {
      domNode.removeEventListener(eventName, oldValue);
    }
    if (newValue) {
      domNode.addEventListener(eventName, newValue);
    }
    return;
  }
  
  // Handle style
  if (propName === 'style') {
    if (newValue) {
      Object.assign(domNode.style, newValue);
    } else {
      domNode.style = null;
    }
    return;
  }
  
  // Handle children (special case)
  if (propName === 'children') {
    return; // Handled separately
  }
  
  // Handle all other props
  if (newValue == null) {
    domNode.removeAttribute(propName);
  } else {
    domNode.setAttribute(propName, newValue);
  }
}

This is a working (though simplified) rendering function. It handles:

  • Text nodes
  • Regular elements
  • Props (including event handlers and style)

But it has a major limitation: it can only do initial renders. It can’t update the DOM when state changes. For that, we need the virtual DOM diffing algorithm.

We’ll implement that in Section 2. First, let’s add component support.

Component Support

In React, a component is a function that returns JSX (or a class with a render method). Our library should support function components.

The key insight: when the type of a virtual DOM node is a function (not a string like 'div'), we should call the function to get its virtual DOM representation.

// Render (updated to support components)
function render(virtualNode, container) {
  // NEW: Check if type is a function (component)
  if (typeof virtualNode.type === 'function') {
    // Call the component function to get its virtual DOM
    const componentVirtualNode = virtualNode.type(virtualNode.props);
    // Recursively render the result
    return render(componentVirtualNode, container);
  }
  
  // ... rest of the function (text nodes, regular elements) ...
}

Let’s test this:

// A component
function App(props) {
  return createElement(
    'div',
    { className: 'app' },
    createElement('h1', {}, `Hello, ${props.name}!`),
    createElement('p', {}, 'Welcome to Miniact.')
  );
}

// Render the component
const appVirtualNode = createElement(App, { name: 'Alice' });
render(appVirtualNode, document.getElementById('root'));

This works! When render encounters a virtual node with type: App (a function), it calls App(props) to get the virtual DOM, then renders that.

But we’re still missing state management. Components need to be able to re-render when state changes. For that, we need our own version of useState.

State Management (Miniact.useState)

The useState hook is React’s primary state management tool. Let’s implement a simplified version.

The core idea: each component can have state, and when state changes, the component re-renders.

We need:

  1. A way to store state for each component
  2. A way to update state and trigger re-renders

Here’s a simplified implementation:

// Global state (simplified - in reality, you'd use a fiber-like structure)
let currentComponent = null;
let hookIndex = 0;
const hooks = [];

// The useState hook
function useState(initialValue) {
  const index = hookIndex;
  
  // Initialize hook if first render
  if (!hooks[index]) {
    hooks[index] = initialValue;
  }
  
  // The setState function
  const setState = (newValue) => {
    hooks[index] = newValue;
    // Trigger re-render (simplified - just re-render everything)
    rerender();
  };
  
  hookIndex++; // Move to next hook
  
  return [hooks[index], setState];
}

// Render a component (tracking current component for hooks)
function renderComponent(Component, props) {
  currentComponent = Component;
  hookIndex = 0; // Reset hook index for this component
  
  // Call the component function
  const virtualNode = Component(props);
  
  currentComponent = null;
  
  return virtualNode;
}

This is extremely simplified. Real React uses a fiber linked list to track state per component, and it batches state updates for performance. But the core idea is here: hooks are stored in an array, indexed by call order.

Let’s see it in action:

// Counter component using our useState
function Counter() {
  const [count, setCount] = useState(0);
  
  return createElement(
    'div',
    {},
    createElement('p', {}, `Count: ${count}`),
    createElement('button', { onClick: () => setCount(count + 1) }, 'Increment')
  );
}

When the button is clicked, setCount is called, which updates the hook state and triggers a re-render.

But we’re still re-rendering the entire app on every state change. That’s inefficient. What we need is the virtual DOM diffing algorithm—which we’ll implement in Section 2.

Bringing It All Together

Let’s create the Miniact object that exposes our API:

const Miniact = {
  createElement,
  render,
  useState,
  // ... other exports ...
};

// Configure Babel to use Miniact.createElement
// .babelrc:
// {
//   "plugins": [
//     ["@babel/plugin-transform-react-jsx", { "pragma": "Miniact.createElement" }]
//   ]
// }

export default Miniact;

Now you can use JSX with your library:

import Miniact from './miniact';

function App() {
  const [count, setCount] = Miniact.useState(0);
  
  return (
    <div className="app">
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Miniact.render(<App />, document.getElementById('root'));

This is a working (though very simplified) React-like library. In the next section, we’ll implement the virtual DOM diffing algorithm to make updates efficient.

Miniact Architecture

flowchart TD
    A[JSX Code] -->|Babel compiles to| B[createElement calls]
    B --> C[Virtual DOM Tree]
    C --> D[Render to Real DOM]
    D --> E[Browser Display]
    E --> F[User Interaction]
    F --> G[setState Called]
    G --> H[Re-render Component]
    H --> I[New Virtual DOM Tree]
    I --> J[Diff with Old Tree]
    J --> K[Update Only Changed Nodes]
    K --> E
    
    style A fill:#e1f5fe
    style C fill:#c8e6c9
    style J fill:#fff9c4

This diagram shows the architecture of our Miniact library. The key insight: we don’t re-render the entire DOM on every state change—we diff the old and new virtual DOM trees, and update only what changed.

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here

Share your love