# Build a Mini UI Framework - The Magic, Demystified

> Build the machinery inside React, Vue, and Svelte yourself in ~120 lines of plain JavaScript: a proxy-based reactivity system, effects and computed values, a virtual DOM, and a diff.


---

# Build a Mini UI Framework - The Magic, Demystified

You've used a frontend framework - maybe you've read our [React](../../frontend/react-from-zero/_guide.md),
[Vue](../../frontend/vue-from-zero/_guide.md), or [Svelte](../../frontend/svelte-from-zero/_guide.md)
guides - and somewhere in your head there's still a box labeled "magic." *How* does changing
`count` update the screen? What *is* a virtual DOM, physically? What does "tracking dependencies"
actually track?

This project empties the box. In about 120 lines of plain JavaScript, run right here in your
browser, you'll build the load-bearing machinery of a modern UI framework: a reactivity system
that knows who read what (Vue's engine, Svelte's engine), an effect and computed layer on top of
it, a virtual DOM with a render function (React's core idea), and a diff that finds the minimal
set of changes. At the end you'll wire them into one working micro-framework and map each piece
onto the real frameworks' names for it.

Nothing here is a toy in the pejorative sense - these are the *actual algorithms*, minus the
production hardening. After this project, framework documentation reads like a description of
code you've written.

## What you need

Comfortable JavaScript: objects, functions as values, arrays, `map`/`filter`. Having read one of
the frontend from-zero guides helps you connect the dots but isn't required - this project also
works as a *prequel* that makes those guides land harder.

## How to read this

Every phase builds and runs real code in the page. Run every block - and do the exercises before
peeking at the next block, because each phase's machinery becomes the next phase's raw material.

## The phases

1. **[Reactive Objects](01-reactive-objects.md)** - a Proxy that notices reads and writes: the
   atom of all reactivity.
2. **[Effects and Computed](02-effects-and-computed.md)** - automatic dependency tracking, the
   trick every framework shares.
3. **[The Virtual DOM](03-the-virtual-dom.md)** - UI as cheap description objects, and a render
   function.
4. **[The Diff](04-the-diff.md)** - comparing two descriptions to find the minimal change - and
   why keys exist.
5. **[Wiring It Together](05-wiring-it-together.md)** - state → render → diff as one loop, and
   the map from your 120 lines to React, Vue, Svelte, and Angular.


---

# Reactive Objects

Every reactivity system - Vue's, Svelte's, the one you're about to write - rests on one
capability: **knowing when data is read and when it's written.** Plain JavaScript objects don't
report either. But the language ships a tool that wraps any object and intercepts everything done
to it: the `Proxy`. Today you build with it.

## Meet the Proxy

A `Proxy` wraps a target object and routes operations through *traps* - functions you supply.
Two traps carry this whole project: `get` (a property was read) and `set` (a property was
written). Run it:

```js runnable
const user = { name: 'Ada', plan: 'pro' };

const spied = new Proxy(user, {
  get(target, key) {
    console.log(`READ  ${String(key)}`);
    return target[key];
  },
  set(target, key, value) {
    console.log(`WRITE ${String(key)} = ${value}`);
    target[key] = value;
    return true; // set traps must return true on success
  },
});

// Use it like a normal object - the traps fire invisibly:
const n = spied.name;
spied.plan = 'enterprise';
console.log('Reads and writes went through, value is:', spied.plan);
```

*What just happened:* `spied` behaves exactly like `user` - same properties, same values - but
every access ran through your traps first. The object can now *announce* its own reads and
writes. That announcement is the entire foundation: a framework that hears "someone read `name`"
and later "someone wrote `name`" knows exactly which screen updates matter.

## From spy to subscription ledger

Logging is a demo; a framework needs bookkeeping. The plan: when a property is read, remember
*who was asking* (we'll wire that up properly in phase 2 - for now, a placeholder). When it's
written, look up everyone who asked and notify them. The ledger:

```js runnable
// A two-level map: object -> (key -> Set of subscribers)
const ledger = new WeakMap();

function subscribe(target, key, fn) {
  let keyMap = ledger.get(target);
  if (!keyMap) ledger.set(target, (keyMap = new Map()));
  let subs = keyMap.get(key);
  if (!subs) keyMap.set(key, (subs = new Set()));
  subs.add(fn);
}

function notify(target, key) {
  const subs = ledger.get(target)?.get(key);
  if (subs) for (const fn of subs) fn();
}

// Try the ledger on its own:
const state = { count: 0 };
subscribe(state, 'count', () => console.log('count changed!'));
subscribe(state, 'count', () => console.log('me too!'));
notify(state, 'count');
```

*What just happened:* `subscribe` files a function under (object, property); `notify` runs
everything filed there. A `WeakMap` keyed by the object means the bookkeeping disappears when the
object does - no leak. A `Set` per property means the same subscriber can't be filed twice.

## reactive(): the two pieces joined

Now the move that makes it automatic - the proxy's traps *call* the ledger:

```js runnable
const ledger = new WeakMap();

function subscribe(target, key, fn) {
  let keyMap = ledger.get(target);
  if (!keyMap) ledger.set(target, (keyMap = new Map()));
  let subs = keyMap.get(key);
  if (!subs) keyMap.set(key, (subs = new Set()));
  subs.add(fn);
}

function notify(target, key) {
  const subs = ledger.get(target)?.get(key);
  if (subs) for (const fn of subs) fn();
}

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      // Phase 2 will subscribe the *current effect* here automatically.
      return target[key];
    },
    set(target, key, value) {
      if (target[key] === value) return true;  // no change, no noise
      target[key] = value;
      notify(target, key);                     // announce to subscribers
      return true;
    },
  });
}

// Demo: manual subscription, automatic notification.
// Note we subscribe on the RAW object: the traps file their bookkeeping under it
// (the `target` they receive), so lookups must use the same key. Phase 2's
// automatic tracking happens inside the traps, which makes this seam invisible.
const rawCart = { items: 0, total: 0 };
const cart = reactive(rawCart);

subscribe(rawCart, 'items', () => console.log(`UI update: badge shows ${cart.items}`));
subscribe(rawCart, 'total', () => console.log(`UI update: total shows ${cart.total}`));

cart.items = 1;      // only the badge subscriber fires
cart.total = 1900;   // only the total subscriber fires
cart.items = 1;      // same value: nothing fires (check the guard in set)
cart.items = 2;
```

*What just happened:* writes now notify precisely the subscribers of *that property* - update
`items` and the total subscriber stays quiet. The `target[key] === value` guard skips no-op
writes, which real frameworks also do (you met it as "signals compare by reference" if you've
read the Angular guide). Two loose ends remain, both deliberate: the subscribing is still manual,
and you had to know about the raw-vs-proxy seam to file subscriptions under the right key. Phase 2
fixes both at once - automatic tracking lives *inside* the traps, where `target` is always the raw
object - and it's the best trick in frontend engineering.

## Your turn

Extend `reactive`'s `set` trap so it also logs a warning - without notifying - when code tries to
write a property that didn't exist on the original object (a typo catcher: `cart.tota = 5`
should warn, not silently create a property). `Object.hasOwn(target, key)` tells you if the key
existed. Then prove it works:

```js runnable
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      return target[key];
    },
    set(target, key, value) {
      // YOUR CODE: if the key is NOT already on target, console.log a warning
      // like `unknown property "tota" - typo?` and return true WITHOUT writing.
      // Otherwise write and return true (skip notify - no ledger in this block).
      target[key] = value;
      return true;
    },
  });
}

const cart = reactive({ items: 0, total: 0 });
cart.items = 3;      // should write silently
cart.tota = 99;      // should WARN and not create the property
console.log('items:', cart.items, '| tota exists:', 'tota' in cart); // 3, false
```

## Recap

1. A `Proxy` wraps an object and intercepts reads (`get`) and writes (`set`) - data that can
   announce itself.
2. The ledger is a `WeakMap(object → Map(key → Set(subscriber)))` - per-property precision,
   leak-free by construction.
3. `reactive()` = proxy + ledger: writes notify exactly the right subscribers, no-op writes are
   filtered.
4. Reads are the missing half - automating "who was asking" is phase 2.


---

# Effects and Computed

Phase 1 left one thing manual: `subscribe(cart, 'items', fn)` - you had to *say* which properties
a function cares about. Real frameworks never ask. A Vue template, a Svelte `$effect`, an Angular
`computed` - they all figure out their own dependencies. The mechanism behind all of them fits in
one sentence, and it's the best trick in frontend engineering:

💡 **Key point:** keep a global variable pointing at *the function currently running*. While it
runs, every reactive property it reads sees that global in the `get` trap - and subscribes it.
Reading **is** subscribing.

## effect(): the trick, implemented

```js runnable
// --- the machinery from phase 1, plus the one new variable ---
const ledger = new WeakMap();
let activeEffect = null;                       // ← the whole trick

function track(target, key) {
  if (!activeEffect) return;                   // nobody's running? nothing to file.
  let keyMap = ledger.get(target);
  if (!keyMap) ledger.set(target, (keyMap = new Map()));
  let subs = keyMap.get(key);
  if (!subs) keyMap.set(key, (subs = new Set()));
  subs.add(activeEffect);
}

function notify(target, key) {
  const subs = ledger.get(target)?.get(key);
  if (subs) for (const fn of [...subs]) fn();
}

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key);                      // reads file the current effect
      return target[key];
    },
    set(target, key, value) {
      if (target[key] === value) return true;
      target[key] = value;
      notify(target, key);
      return true;
    },
  });
}

function effect(fn) {
  activeEffect = fn;                           // announce "I'm running"
  fn();                                        // reads inside subscribe fn automatically
  activeEffect = null;                         // stop announcing
}

// --- watch it work ---
const user = reactive({ first: 'Ada', last: 'Lovelace', visits: 0 });

effect(() => {
  console.log(`Hello, ${user.first} ${user.last}!`);   // reads first + last → subscribes to both
});

user.first = 'Grace';    // effect re-runs
user.last = 'Hopper';    // effect re-runs
user.visits = 41;        // effect does NOT re-run - it never read visits
```

*What just happened:* the effect ran once, and its two reads (`first`, `last`) filed it in the
ledger *because `activeEffect` was set while they happened*. Writing either re-runs it; writing
`visits` doesn't, because the effect never read it - **precision without declaration**. No
dependency array, no subscribe calls: the reads are the truth about what the function needs, so
the reads are what's tracked.

If you've read the frontend guides, name what you just built: Vue's template tracking works this
way (reads through the proxy register the template), Svelte's `$effect` auto-tracking works this
way, Angular's signal graph works this way. Different syntax, same `activeEffect` at the center.

## Your turn: the stale-subscription hunt

One subtlety separates your effect from production ones. Predict first, then run:

```js runnable
// (compact copy of the machinery)
const ledger = new WeakMap(); let activeEffect = null;
function track(t, k) { if (!activeEffect) return; let m = ledger.get(t); if (!m) ledger.set(t, m = new Map()); let s = m.get(k); if (!s) m.set(k, s = new Set()); s.add(activeEffect); }
function notify(t, k) { const s = ledger.get(t)?.get(k); if (s) for (const f of [...s]) f(); }
function reactive(o) { return new Proxy(o, { get(t, k) { track(t, k); return t[k]; }, set(t, k, v) { if (t[k] === v) return true; t[k] = v; notify(t, k); return true; } }); }
function effect(fn) { activeEffect = fn; fn(); activeEffect = null; }

const state = reactive({ loggedIn: true, name: 'Ada' });

effect(() => {
  // A branching effect: reads different properties depending on loggedIn.
  if (state.loggedIn) console.log(`Welcome, ${state.name}`);
  else console.log('Please log in');
});

state.loggedIn = false;   // effect re-runs, prints "Please log in"
state.name = 'Grace';     // QUESTION: does the effect re-run? Should it?
```

*What just happened:* it re-ran - and printed "Please log in" again, uselessly. The first run
subscribed to `name`; after `loggedIn` flipped, the effect doesn't read `name` anymore, but the
old subscription is still filed. Production frameworks fix this by **clearing an effect's old
subscriptions before every re-run**, so each run's reads define its dependencies fresh. That's
"dependencies are what you read *last time*" - a line you can now read in Vue's or Svelte's
source and nod at. (Implementing the cleanup is a great stretch exercise: give each effect a list
of the Sets it's been added to, and empty them before re-running.)

## computed(): a cached effect with a value

A derived value is an effect that *produces* something instead of doing something - plus a cache:

```js runnable
const ledger = new WeakMap(); let activeEffect = null;
function track(t, k) { if (!activeEffect) return; let m = ledger.get(t); if (!m) ledger.set(t, m = new Map()); let s = m.get(k); if (!s) m.set(k, s = new Set()); s.add(activeEffect); }
function notify(t, k) { const s = ledger.get(t)?.get(k); if (s) for (const f of [...s]) f(); }
function reactive(o) { return new Proxy(o, { get(t, k) { track(t, k); return t[k]; }, set(t, k, v) { if (t[k] === v) return true; t[k] = v; notify(t, k); return true; } }); }
function effect(fn) { activeEffect = fn; fn(); activeEffect = null; }

function computed(fn) {
  let cached;
  let dirty = true;                       // "the cache is stale"
  // An effect subscribes computed's recalculation flag to fn's dependencies:
  effect(() => {
    fn();                                 // dry run purely to register dependencies
    dirty = true;                         // any dependency change re-marks stale
  });
  return {
    get value() {
      if (dirty) {
        console.log('  (recomputing...)');
        cached = fn();
        dirty = false;
      }
      return cached;
    },
  };
}

const cart = reactive({ price: 1900, qty: 2 });
const total = computed(() => cart.price * cart.qty);

console.log('total:', total.value);   // recomputes
console.log('total:', total.value);   // cached - no recompute line
cart.qty = 3;                         // marks dirty (via the inner effect)
console.log('total:', total.value);   // recomputes once
console.log('total:', total.value);   // cached again
```

*What just happened:* the `dirty` flag is the whole idea of `computed` in every framework - **lazy
recomputation**. Reading a clean cache is free; a dependency write only flips the flag; the next
read pays the recompute once. Ten template references to a computed cost one calculation - the
exact behavior our React (`useMemo`), Vue (`computed`), Svelte (`$derived`), and Angular
(`computed`) guides described from the outside. (Production versions propagate "dirtiness"
through chains of computeds more cleverly - the flag is the real core.)

## Recap

1. The trick: a global `activeEffect` set while a function runs; `get` traps file it. Reading is
   subscribing.
2. Precision falls out: effects re-run only for properties they actually read.
3. The branching-effect exercise shows why real frameworks re-track on every run - dependencies
   are last run's reads.
4. `computed` = effect + cache + dirty flag: lazy, cached derivation.
5. You have now personally implemented the sentence "the framework tracks your dependencies."


---

# The Virtual DOM

Phases 1-2 built the *when* - knowing the moment data changes. Now the *what*: what should the
screen look like? React's founding idea (which our
[React guide's phase 1](../../frontend/react-from-zero/01-what-react-actually-is.md) described
from the outside) is that UI should be a **description** - a cheap, throwaway JavaScript object -
produced fresh from your data every time. Today you build the describer; phase 4 compares
descriptions; phase 5 connects them to your reactivity engine.

## h(): the element factory

A UI description needs three facts per element: what tag, what attributes, what's inside. So:

```js runnable
function h(type, props, ...children) {
  return { type, props: props || {}, children: children.flat() };
}

// Describe a product card - no DOM involved, this is just data:
const card = h('article', { class: 'card' },
  h('h3', null, 'Kettle'),
  h('p', { class: 'price' }, '19.00 €'),
  h('button', { disabled: false }, 'Add to cart'),
);

console.log(JSON.stringify(card, null, 2));
```

*What just happened:* `h` (the traditional name - "hyperscript") builds a plain object: a
**virtual node**, or *vnode*. Children can be more vnodes or bare strings (text). `...children`
gathers everything after props; `.flat()` lets callers pass arrays (you'll see why in a moment).
The printout is the whole point: **your UI is now data** - inspectable, comparable, cheap to make
and throw away.

📝 **Terminology:** when our React guide said JSX compiles to `createElement` calls returning
"description objects" - this is that, minus the compiler. `<h3>Kettle</h3>` and
`h('h3', null, 'Kettle')` are the same sentence in two spellings.

## Describing dynamically: it's just JavaScript

Because descriptions are built by function calls, all of JavaScript is your template language:

```js runnable
function h(type, props, ...children) {
  return { type, props: props || {}, children: children.flat() };
}

function TodoList({ todos, filter }) {
  const shown = filter === 'all' ? todos : todos.filter(t => !t.done);
  return h('div', { class: 'todos' },
    h('h2', null, `${shown.length} of ${todos.length} tasks`),
    shown.length === 0
      ? h('p', null, 'Nothing here.')                    // conditional: a ternary
      : h('ul', null,
          shown.map(t => h('li', null, t.text)),          // a list: map (hence .flat()!)
        ),
  );
}

const vtree = TodoList({
  todos: [ { text: 'Build h()', done: true }, { text: 'Build render()', done: false } ],
  filter: 'active',
});

console.log(JSON.stringify(vtree, null, 2));
```

*What just happened:* `TodoList` is a **component** - a plain function from data to description,
which is all a component fundamentally is. Conditionals are ternaries, lists are `map` - the
exact idioms our React guide teaches, revealed as ordinary code because that's all they ever
were. The `map` returns an array of vnodes inside the children list - which is why `h` flattens.

## render(): description to HTML

A description is only useful if something realizes it. Browsers realize DOM nodes; for a runnable
console project, we'll realize HTML text - the logic is identical, minus `document.createElement`:

```js runnable
function h(type, props, ...children) {
  return { type, props: props || {}, children: children.flat() };
}

function renderToString(vnode) {
  if (vnode == null || vnode === false) return '';        // skip empty branches
  if (typeof vnode === 'string' || typeof vnode === 'number') return String(vnode);

  const attrs = Object.entries(vnode.props)
    .filter(([, v]) => v !== false && v != null)           // false/null props: omitted
    .map(([k, v]) => (v === true ? ` ${k}` : ` ${k}="${v}"`))
    .join('');

  const inner = vnode.children.map(renderToString).join('');
  return `<${vnode.type}${attrs}>${inner}</${vnode.type}>`;
}

const page = h('main', null,
  h('h1', { class: 'hero' }, 'Mini Framework'),
  h('button', { disabled: true }, 'Ship it'),
  false && h('p', null, 'never rendered'),                 // conditional that's off
);

console.log(renderToString(page));
```

*What just happened:* a recursive walk - strings render as themselves, vnodes render as a tag,
its attributes, and its recursively-rendered children. Notice two framework behaviors emerging
naturally: `false`/`null` children render as nothing (that's why `{cond && <X/>}` works in JSX),
and boolean props render as bare attributes (`disabled`) or vanish (`disabled: false`) - the
attribute-vs-property care our Angular guide's phase 2 made a fuss about, now visible from the
implementing side.

## Your turn

Real renderers must escape text - otherwise user data containing `<` breaks the page (or worse:
injected scripts - this is XSS, the reason React strings are safe by default). Add escaping:

```js runnable
function h(type, props, ...children) {
  return { type, props: props || {}, children: children.flat() };
}

function escapeHtml(s) {
  // YOUR CODE: return s with & < > " replaced by &amp; &lt; &gt; &quot;
  // (order matters: & first, or you'll double-escape the others!)
  return s;
}

function renderToString(vnode) {
  if (vnode == null || vnode === false) return '';
  if (typeof vnode === 'string' || typeof vnode === 'number') return escapeHtml(String(vnode));
  const attrs = Object.entries(vnode.props)
    .filter(([, v]) => v !== false && v != null)
    .map(([k, v]) => (v === true ? ` ${k}` : ` ${k}="${v}"`))
    .join('');
  return `<${vnode.type}${attrs}>${vnode.children.map(renderToString).join('')}</${vnode.type}>`;
}

// A user "typed" this into a comment box:
const evil = h('p', null, '<script>steal(cookies)</script> & fun');
console.log(renderToString(evil));
// Goal: <p>&lt;script&gt;steal(cookies)&lt;/script&gt; &amp; fun</p>
```

## Recap

1. `h(type, props, ...children)` builds vnodes - plain objects describing UI. JSX is this with
   nicer clothes.
2. Components are functions from data to vnode trees; conditionals and lists are just JavaScript.
3. A renderer is a recursive walk realizing descriptions - as HTML here, as DOM nodes in the real
   thing; skipping `false`/`null` is why `&&`-rendering works.
4. Escaping text at render time is why frameworks are XSS-safe by default.
5. Descriptions are cheap and comparable - and comparing two of them is phase 4.


---

# The Diff

You can now produce a fresh description of the whole UI after every change. Realizing the whole
description every time would work - and throw away every input's text, every scroll position,
every video's playback, while doing a page worth of work for a one-word change. The fix is the
algorithm at the heart of React and Vue: **compare the new description to the old one, and change
only what differs.** Today you write it - and personally trigger the bug that made every
framework demand keys.

## diff(): the recursive compare

Our diff walks two vnode trees and emits *patch operations* - console-friendly descriptions of
what a real renderer would do to the DOM:

```js runnable
function h(type, props, ...children) {
  return { type, props: props || {}, children: children.flat() };
}

function diff(oldNode, newNode, path = 'root') {
  const patches = [];

  // 1. Something vs nothing:
  if (oldNode == null) { patches.push(`CREATE ${path}: ${describe(newNode)}`); return patches; }
  if (newNode == null) { patches.push(`REMOVE ${path}: ${describe(oldNode)}`); return patches; }

  // 2. Text nodes: compare content.
  const oldIsText = typeof oldNode !== 'object';
  const newIsText = typeof newNode !== 'object';
  if (oldIsText || newIsText) {
    if (oldNode !== newNode) patches.push(`TEXT   ${path}: "${oldNode}" -> "${newNode}"`);
    return patches;
  }

  // 3. Different tag entirely: no way to patch - replace the subtree.
  if (oldNode.type !== newNode.type) {
    patches.push(`REPLACE ${path}: <${oldNode.type}> -> <${newNode.type}>`);
    return patches;
  }

  // 4. Same tag: compare props...
  const keys = new Set([...Object.keys(oldNode.props), ...Object.keys(newNode.props)]);
  for (const k of keys) {
    if (oldNode.props[k] !== newNode.props[k]) {
      patches.push(`PROP   ${path}: ${k}: ${oldNode.props[k]} -> ${newNode.props[k]}`);
    }
  }

  // 5. ...and recurse into children, matched BY POSITION (remember this!).
  const len = Math.max(oldNode.children.length, newNode.children.length);
  for (let i = 0; i < len; i++) {
    patches.push(...diff(oldNode.children[i], newNode.children[i], `${path}/${newNode?.type}[${i}]`));
  }
  return patches;
}

function describe(n) { return typeof n === 'object' && n ? `<${n.type}>` : `"${n}"`; }

// --- a small change between two renders ---
const before = h('main', null,
  h('h1', null, 'Cart (2)'),
  h('button', { disabled: false }, 'Checkout'),
);
const after = h('main', null,
  h('h1', null, 'Cart (3)'),
  h('button', { disabled: false }, 'Checkout'),
);

diff(before, after).forEach(p => console.log(p));
```

*What just happened:* one text patch. The whole page was re-*described*, but the diff found that
only the heading's text differs - the button, its props, everything else produced zero
operations. That's the virtual DOM bargain in one console line: describe everything, touch
almost nothing. The rules you implemented - text compare, type mismatch = replace subtree, same
type = patch props and recurse - are React's reconciliation heuristics, straight from their
docs, in your handwriting.

## Now break it

Step 5 matched children *by position* - old[0] vs new[0]. Watch what that does to a reordered
list. Same `diff` (compact), a list that moves its first item to the end:

```js runnable
function h(t, p, ...c) { return { type: t, props: p || {}, children: c.flat() }; }
function describe(n) { return typeof n === 'object' && n ? `<${n.type}>` : `"${n}"`; }
function diff(o, n, path = 'root') {
  const P = [];
  if (o == null) { P.push(`CREATE ${path}: ${describe(n)}`); return P; }
  if (n == null) { P.push(`REMOVE ${path}: ${describe(o)}`); return P; }
  if (typeof o !== 'object' || typeof n !== 'object') { if (o !== n) P.push(`TEXT   ${path}: "${o}" -> "${n}"`); return P; }
  if (o.type !== n.type) { P.push(`REPLACE ${path}`); return P; }
  const keys = new Set([...Object.keys(o.props), ...Object.keys(n.props)]);
  for (const k of keys) if (o.props[k] !== n.props[k]) P.push(`PROP   ${path}: ${k}`);
  const len = Math.max(o.children.length, n.children.length);
  for (let i = 0; i < len; i++) P.push(...diff(o.children[i], n.children[i], `${path}[${i}]`));
  return P;
}

const before = h('ul', null,
  h('li', null, 'Buy milk'),
  h('li', null, 'Walk dog'),
  h('li', null, 'Call mom'),
);
// The user "moved" Buy milk to the bottom - same three items, one moved:
const after = h('ul', null,
  h('li', null, 'Walk dog'),
  h('li', null, 'Call mom'),
  h('li', null, 'Buy milk'),
);

diff(before, after).forEach(p => console.log(p));
console.log('\nOne item moved. Patches emitted:', diff(before, after).length);
```

*What just happened:* **three** text rewrites for a single move. Position-matching decided
"item 0 changed its text from Buy milk to Walk dog" and so on down the list - it rewrote *every
row's contents* instead of moving one node. In a real DOM those rewrites destroy whatever lived
in those rows: input text, checkbox state, focus. This is - exactly, mechanically - the
index-key corruption bug from our
[React guide's phase 4](../../frontend/react-from-zero/04-lists-keys-and-conditional-rendering.md)
(and Vue's, and Svelte's, and Angular's `track` rule). You've now caused it from the inside.

## Your turn: fix it with keys

Give the differ identity to match by, instead of position. Add key support to the child loop:

```js runnable
function h(t, p, ...c) { return { type: t, props: p || {}, children: c.flat() }; }
function diffChildren(oldChildren, newChildren) {
  const patches = [];
  // YOUR CODE: match children by props.key instead of position.
  // Plan:
  //  1. Build a Map from key -> old child (for old children that have props.key).
  //  2. For each new child: if its key exists in the map at a DIFFERENT index -> "MOVE key=<k>".
  //     If the key isn't in the map -> "CREATE key=<k>".
  //  3. Any old key absent from the new children -> "REMOVE key=<k>".
  // (Ignore non-keyed children in this exercise.)
  return patches;
}

const before = [ h('li', { key: 'milk' }, 'Buy milk'), h('li', { key: 'dog' }, 'Walk dog'), h('li', { key: 'mom' }, 'Call mom') ];
const after  = [ h('li', { key: 'dog' }, 'Walk dog'), h('li', { key: 'mom' }, 'Call mom'), h('li', { key: 'milk' }, 'Buy milk') ];

diffChildren(before, after).forEach(p => console.log(p));
// Goal: exactly one MOVE (milk) - and zero rewrites. That's what keys buy.
```

When your version prints a single `MOVE key=milk`, you've written the reason every framework
demands stable keys: identity turns "rewrite three rows" into "move one node." (Real frameworks'
keyed algorithms also minimize *which* moves - longest-increasing-subsequence tricks - but the
identity insight is the whole foundation.)

## Recap

1. The diff: null checks, text compare, different type = replace, same type = patch props +
   recurse into children.
2. One data change → one patch, even though the whole tree was re-described. That's the bargain.
3. Positional child matching turns a reorder into a cascade of rewrites - you triggered the
   famous keys bug deliberately.
4. Keys give the differ identity: match by key, and a move is a move.
5. Everything our framework guides said about reconciliation and keys, you have now implemented.


---

# Wiring It Together

You have all three machines: reactivity that knows *when* data changes (phases 1-2), a describer
that says *what* the UI should be (phase 3), and a diff that finds *the least* to do about it
(phase 4). A framework is these three in a loop. Today: the loop - and then the satisfying part,
mapping your 120 lines onto the four frameworks' vocabularies.

## The whole framework, assembled

Read top to bottom - every line is something you built - then run it:

```js runnable
// ══ 1. REACTIVITY (phases 1-2) ══════════════════════════════
const ledger = new WeakMap(); let activeEffect = null;
function track(t, k) { if (!activeEffect) return; let m = ledger.get(t); if (!m) ledger.set(t, m = new Map()); let s = m.get(k); if (!s) m.set(k, s = new Set()); s.add(activeEffect); }
function notify(t, k) { const s = ledger.get(t)?.get(k); if (s) for (const f of [...s]) f(); }
function reactive(o) { return new Proxy(o, { get(t, k) { track(t, k); return t[k]; }, set(t, k, v) { if (t[k] === v) return true; t[k] = v; notify(t, k); return true; } }); }
function effect(fn) { activeEffect = fn; fn(); activeEffect = null; }

// ══ 2. DESCRIPTION (phase 3) ════════════════════════════════
function h(t, p, ...c) { return { type: t, props: p || {}, children: c.flat() }; }
function renderToString(v) {
  if (v == null || v === false) return '';
  if (typeof v !== 'object') return String(v);
  const attrs = Object.entries(v.props).filter(([, x]) => x !== false && x != null)
    .map(([k, x]) => (x === true ? ` ${k}` : ` ${k}="${x}"`)).join('');
  return `<${v.type}${attrs}>${v.children.map(renderToString).join('')}</${v.type}>`;
}

// ══ 3. DIFF (phase 4, compact) ══════════════════════════════
function diff(o, n, path = 'root') {
  const P = [];
  if (o == null) { P.push(`CREATE ${path}`); return P; }
  if (n == null) { P.push(`REMOVE ${path}`); return P; }
  if (typeof o !== 'object' || typeof n !== 'object') { if (o !== n) P.push(`TEXT ${path}: "${o}" -> "${n}"`); return P; }
  if (o.type !== n.type) { P.push(`REPLACE ${path}`); return P; }
  for (const k of new Set([...Object.keys(o.props), ...Object.keys(n.props)]))
    if (o.props[k] !== n.props[k]) P.push(`PROP ${path}: ${k} -> ${n.props[k]}`);
  const len = Math.max(o.children.length, n.children.length);
  for (let i = 0; i < len; i++) P.push(...diff(o.children[i], n.children[i], `${path}[${i}]`));
  return P;
}

// ══ 4. THE LOOP: mount() ties them together ═════════════════
function mount(component, state) {
  let oldTree = null;
  effect(() => {                        // ← re-runs whenever state it reads changes
    const newTree = component(state);   // describe (reads state → subscribes!)
    if (oldTree === null) {
      console.log('MOUNT:', renderToString(newTree));
    } else {
      const patches = diff(oldTree, newTree);
      console.log(patches.length ? 'PATCH: ' + patches.join(' | ') : 'PATCH: (nothing)');
    }
    oldTree = newTree;
  });
}

// ══ 5. AN APP - just a component and state ══════════════════
const state = reactive({ count: 0, title: 'Clicks' });

function App(s) {
  return h('main', null,
    h('h1', null, s.title),
    h('button', { disabled: s.count >= 3 }, `Clicked ${s.count} times`),
  );
}

mount(App, state);

// Simulate three "clicks" and a rename - watch the patches:
state.count = 1;
state.count = 2;
state.count = 3;      // count hits the limit: TEXT change AND disabled flips - one write, two patches
state.title = 'Total';
```

*What just happened,* and it's worth savoring: `mount` wraps the component call in an `effect`.
Rendering *reads* `s.count` and `s.title` through the proxy - so the render effect subscribes to
exactly the state the UI uses, automatically. Every later write re-runs the effect, which
re-describes the UI, diffs against the previous description, and reports the minimal change. The
third click is the beauty shot: **one state write produced two coordinated patches** (button text
plus `disabled` flipping) because the description derived both from the same value.

That's a UI framework. State in, minimal updates out, nothing manual in between.

```mermaid
flowchart LR
  W[state.count = 3] --> N[proxy notifies render effect]
  N --> D["App(state) - new description"]
  D --> F[diff vs old description]
  F --> P[minimal patches out]
  P -.->|next write| W
```

## The map: your 120 lines → their million

Now every framework term from our frontend guides has a line number in your own code:

| You built | React calls it | Vue calls it | Svelte calls it | Angular calls it |
|---|---|---|---|---|
| `reactive()` proxy | - (state via setters instead) | `reactive()` / `ref` | `$state` proxies | `signal()` |
| `activeEffect` tracking | - | template/effect tracking | rune auto-tracking | signal graph |
| `effect()` | `useEffect` (cousin) | `watchEffect` | `$effect` | `effect()` |
| `computed()` + dirty flag | `useMemo` | `computed` | `$derived` | `computed()` |
| `h()` / vnodes | `createElement` / JSX | template → vnodes | - (compiled away) | template → instructions |
| `renderToString` | server rendering | SSR | SSR | Angular SSR |
| `diff()` | reconciliation | patching | - (precompiled updates) | change detection |
| keys exercise | `key` | `:key` | keyed `{#each}` | `track` |

The dashes teach as much as the entries. React has no reactive proxy - it re-runs components
wholesale and leans entirely on the diff (which is why it insists on immutable updates: identity
is its only change signal). Svelte has no runtime diff - its compiler knows the dependencies at
build time, so it ships direct updates (your phase 2 ledger, resolved ahead of time). Vue and
Angular sit in between: reactive tracking chooses *which* components re-render, then templates
update efficiently. Four frameworks, one design space - and you've now built enough of it to
place any future framework on the map in an afternoon.

## Where to take it

Stretch goals, each a real weekend project on this foundation:

- **Real DOM.** Replace `renderToString` + patch logs with `document.createElement` and actual
  patch application - the same walk, mutating nodes. (Do it in a scratch HTML file; the logic
  transfers line for line.)
- **Effect re-tracking.** Fix phase 2's stale-subscription exercise properly: clear an effect's
  subscriptions before each run.
- **Keyed diff in the main algorithm.** Merge your phase 4 exercise into `diff` so keyed children
  move instead of rewriting.
- **Read the giants.** Vue's `@vue/reactivity` package is your phases 1-2 with production
  hardening - and it's readable now. So is Preact's diff (a famously compact real-world
  reconciler).

## Recap

1. A framework is a loop: reactive state → render effect describes UI → diff finds minimal
   change. `mount` is fifteen lines.
2. Rendering inside an effect is the keystone: reading state during render *is* the subscription.
3. One write can coordinate many patches because the description derives everything from state.
4. The comparison table is yours now - including why React demands immutability and Svelte
   ships no diff.
5. The magic box is empty. It was code all along - about 120 lines of it.
