Reacting to stimuli is how we react to stimuli
If living things react to stimuli, then so should our blocks.
Topic 4: Reactivity (The Proxy)
LegoDOM uses the JavaScript Proxy object to create its reactivity. Think of a Proxy as a "security guard" that sits in front of your data object. Every time you try to read or change a property, the guard intercepts the request.
Location in the Codebase
The reactive system lives in src/core/reactive.js:
// src/core/reactive.js
import { isDbDescriptor, initializeDbProp, handleDbUpdate } from '../features/persistence.js';
const proxyCache = new WeakMap();
export const reactive = (obj, el, batcher = null) => {
if (obj === null || typeof obj !== 'object' || obj instanceof Node) return obj;
if (proxyCache.has(obj)) return proxyCache.get(obj);
// Initialize $db descriptors before proxying
for (const k in obj) {
if (isDbDescriptor(obj[k])) {
initializeDbProp(obj, k, obj[k], el, batcher);
}
}
const handler = {
get: (t, k) => {
const val = Reflect.get(t, k);
if (val !== null && typeof val === 'object' && !(val instanceof Node)) {
return reactive(val, el, batcher);
}
return val;
},
set: (t, k, v) => {
const old = t[k];
const r = Reflect.set(t, k, v);
if (old !== v) {
// Notify persistence system if this is a $db property
handleDbUpdate(t, k, v);
// Queue render if batcher is available
if (batcher) batcher.add(el);
}
return r;
},
deleteProperty: (t, k) => {
const r = Reflect.deleteProperty(t, k);
if (batcher) batcher.add(el);
return r;
}
};
const p = new Proxy(obj, handler);
proxyCache.set(obj, p);
return p;
};The Proxy Traps
The
gettrap:- When you access a property (e.g.,
state.count), the Proxy checks if that property is also an object. - If it is, it recursively wraps that object in a Proxy too.
- This ensures that "deep" data like
user.profile.nameis also reactive.
- When you access a property (e.g.,
The
settrap:This is the main trigger. When you do
state.count = 5, the Proxy compares theoldvalue with thenewvalue.If they are different, it:
a. Calls
handleDbUpdate()to sync withlocalStorageif this is a$dbpropertyb. Calls
batcher.add(el)to queue a render
The
deletePropertytrap:- Even if you delete a key (e.g.,
delete state.tempData), the Proxy intercepts this and tells the batcher to re-render the UI.
- Even if you delete a key (e.g.,
Handling Special Cases
Objects vs. Nodes
The code explicitly checks instanceof Node. If you try to store a raw HTML element in your state, LegoDOM will not wrap it in a Proxy. This prevents LegoDOM from accidentally trying to "observe" the entire DOM tree, which would crash the browser.
Database Integration
The reactive system integrates with the persistence layer (src/features/persistence.js):
// Check for $db descriptors
if (isDbDescriptor(obj[k])) {
initializeDbProp(obj, k, obj[k], el, batcher);
}This allows you to write:
{
todos: $db('my-todos').default([])
}And have the data automatically persist to localStorage and sync across tabs.
Usage in the Framework
The reactive() function is called from src/core/lifecycle.js during the snap() process:
// src/core/lifecycle.js
import { reactive } from './reactive.js';
import { globalBatcher } from './batcher.js';
el._studs = reactive(baseState, el);Concrete Example
Imagine you have a block defined like this:
Lego.block('counter-app', '<h1>[[ count ]]</h1>', {
count: 0,
increment() { this.count++; }
});The Flow:
Step A: Lego takes that object
{ count: 0, ... }and wraps it in a Proxy viareactive().Step B: You call
increment().Step C: The line
this.count++triggers the Proxy'ssettrap.Step D: The
settrap notices0is now1and callsglobalBatcher.add(thisElement).Step E: The Batcher (from
src/core/batcher.js) schedules a render for the next animation frame.
Why this is "Surgical"
Because the reactive function is passed the specific element (el) it belongs to, it knows exactly which block in the DOM needs to re-render. It doesn't have to guess or refresh the whole page; it targets the specific "Lego block" that owns that data.
This is fundamentally different from Virtual DOM approaches where the entire component tree must be diffed.