Skip to content

Repository files navigation

My Own React

A mini React-like Virtual DOM engine built entirely from scratch in TypeScript. No React, Vue, Preact, or any rendering library — just vanilla TypeScript and browser APIs.

Architecture

src/
├── core/           # Engine internals
│   ├── h.ts        # Hyperscript factory (VNode creation)
│   ├── vnode.ts    # VNode data structure
│   ├── mount.ts    # Initial DOM mounting
│   ├── patch.ts    # DOM patching entry
│   ├── diff.ts     # Reconciliation algorithm (keyed + unkeyed)
│   ├── reconciler.ts # Core patch/mount/unmount orchestration
│   ├── renderer.ts # render() and renderToString() entry points
│   ├── scheduler.ts # Batched async updates (microtask queue)
│   └── component.ts # Stateful class components + lifecycle
├── hooks/          # Hook system
│   ├── hookState.ts # Per-component hook cursor + state storage
│   ├── useEffect.ts # Effect scheduling with cleanup
│   └── useState.ts  # State hook
├── dom/            # DOM operations
│   ├── createElement.ts # createElement / createElementNS
│   ├── props.ts    # Property/attribute diffing
│   └── events.ts   # Centralized event delegation
├── shared/         # Types, utils, constants
├── demo/           # Demo applications
├── router/         # Tiny History API router (stretch goal)
└── tests/          # Vitest + jsdom test suite

How It Works

1. Virtual DOM (h())

Every UI element is a VNode — a plain object describing what to render:

type VNode = {
  type: string | Component | symbol;
  props: Record<string, unknown>;
  children: VNode[];
  key?: string | number;
  el?: Node;           // real DOM reference after mount
  component?: ComponentInstance;
};

The h() hyperscript function creates VNodes. JSX is configured to compile to h() calls.

2. Mounting

mount(vnode, container) walks the VNode tree and creates real DOM nodes:

  • HTML elements via document.createElement
  • SVG elements via document.createElementNS
  • Text nodes via document.createTextNode
  • Fragments use a comment anchor (no wrapper element)

3. Diffing & Reconciliation

patch(oldVNode, newVNode, container) is the core algorithm:

  1. Same type → patch props/children in place
  2. Different type → unmount old, mount new
  3. Keyed children → key map lookup + reverse-order DOM insertion
  4. Unkeyed children → index-based diff

Keyed list reconciliation uses a three-pass algorithm:

  • Pass 1: Match by key, patch existing nodes
  • Pass 2: Mount new nodes
  • Pass 3: Insert from end-to-start to establish correct DOM order

4. Batched Updates

Multiple setState() calls in the same tick are batched into one rerender via queueMicrotask:

setState({ count: 1 });
setState({ count: 2 });
setState({ count: 3 });
// → ONE reconciliation cycle, final count = 3

5. Event Delegation

Instead of attaching listeners on every element, one listener per event type is attached at the root container. Events bubble up and are dispatched to the correct handler via a WeakMap<Node, handlers>.

6. Hooks

A minimal hooks system with per-component hook index tracking:

  • useState(initial) — component state with setter
  • useEffect(fn, deps) — side effects with cleanup and dependency tracking

7. Lifecycle Hooks

Class components support:

  • onMount() — after first render
  • onUpdate(prevProps, prevState) — after each update
  • onDestroy() — before unmount (with effect cleanup)

Quick Start

npm install
npm run dev      # Start dev server at http://localhost:5173
npm test         # Run test suite (39 tests)
npm run build    # Production build

Usage

import { h, render, Component, useState, useEffect } from './index';

// Class component
class Counter extends Component {
  state = { count: 0 };
  render() {
    return h('button', {
      onClick: () => this.setState({ count: this.state.count + 1 })
    }, [`Count: ${this.state.count}`]);
  }
}

// Functional component with hooks
function Greeting(props: { name: string }) {
  const [greet, setGreet] = useState('Hello');
  useEffect(() => {
    console.log('mounted');
    return () => console.log('cleanup');
  }, []);
  return h('h1', {}, [`${greet}, ${props.name}`]);
}

// Render
render(h(Counter, {}), document.getElementById('app')!);

JSX Support

Configured via tsconfig.json:

{
  "jsx": "react",
  "jsxFactory": "h",
  "jsxFragmentFactory": "Fragment"
}

This allows writing <App /> which compiles to h(App, {}).

Demo Application

The demo includes three tabs:

  1. Todo App — dynamic keyed lists, filters, input handling
  2. Counters — class + functional components, batched updates
  3. SVG Demo — SVG rendering via createElementNS

Server-Side Rendering

import { renderToString, h } from './index';

const html = renderToString(h('div', { class: 'app' }, ['Hello']));
// → '<div class="app">Hello</div>'

Router (Stretch Goal)

A minimal client-side router using the History API:

import { Router, Link } from './router/router';

const router = h(Router, {
  routes: [
    { path: '/', component: () => h(Home, {}) },
    { path: '/about', component: () => h(About, {}) },
  ]
});

Testing

39 tests covering:

  • VNode creation and normalization
  • Mounting and DOM creation
  • Text and prop patching
  • Keyed list reconciliation (reorder, insert, delete)
  • Batched setState
  • useEffect lifecycle and cleanup
  • Class component lifecycle hooks
  • SVG namespace rendering
  • SSR serialization

Performance Characteristics

Operation Complexity
Unkeyed diff O(n)
Keyed diff O(n) with key map
DOM moves Minimized via end-to-start insertion
Event handling O(1) per event type at root
Batched updates O(1) scheduler flush per tick

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages