Reactivity
Understand how Lego makes your UI automatically update when data changes.
The Core Concept
When you change an object, the DOM updates automatically:
block.state.count = 5; // DOM updates!
block.state.items.push('new item'); // DOM updatesNo setState(), no dispatch(), no special syntax. Just mutate the data.
How It Works
Lego uses ES6 Proxies to track changes:
const reactive = (obj, el) => {
return new Proxy(obj, {
// ...
});
};When you set a property, the proxy intercepts it and schedules a re-render.
Surgical Updates
LegoDOM uses scoped rendering—only the block whose state changed gets re-rendered.
┌─────────────────────────────────────────────┐
│ 1000 Blocks on Page │
│ │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ │ │ │ │ ✏️ │ │ │ │ │ │
│ └─────┘ └─────┘ └──┬──┘ └─────┘ └─────┘ │
│ │ │
│ state.count++ │
│ │ │
│ ▼ │
│ Only THIS block │
│ re-renders (1/1000) │
└─────────────────────────────────────────────┘Why It Works This Way
Each block's state is wrapped in its own Proxy, bound to that specific element:
// Internally, LegoDOM does this:
el._studs = reactive(stateObject, el); // Proxy is bound to THIS elementWhen you mutate this.count, the Proxy's set trap only queues that element for re-render—not the entire page.
Performance Implications
| Scenario | Blocks Re-Rendered |
|---|---|
| Block A's local state changes | 1 (Block A only) |
Lego.globals.theme changes | Only blocks using [[ global.theme ]] |
| 1000 blocks, 1 state change | 1 block ✅ |
Global State Optimization
When you modify Lego.globals, LegoDOM doesn't blindly re-render everything. It tracks which blocks have a global dependency (i.e., they reference global.* in their template) and only updates those:
Lego.globals.theme = 'dark'; // Only blocks with [[ global.* ]] bindings updateThis gives you application-wide state without application-wide re-renders.
What's Reactive
✅ Direct Property Assignment
this.count = 10; // ✅ Reactive
this.user.name = 'Alice'; // ✅ Reactive
this.items[0] = 'updated'; // ✅ Reactive✅ Array Methods
this.items.push('new'); // ✅ Reactive
this.items.pop(); // ✅ Reactive
this.items.splice(0, 1); // ✅ Reactive
this.items.sort(); // ✅ Reactive✅ Nested Objects
this.user.profile.age = 30; // ✅ ReactiveLego recursively wraps nested objects in proxies.
✅ Object Deletion
delete this.user.email; // ✅ Reactive💡 Best Practice: Initialize Properties Upfront
While adding new properties at runtime IS reactive, it's still good practice to initialize all properties in your initial state for clarity and maintainability:
{
count: 0,
newProp: null // ✅ Explicitly declared, easy to see what state exists
}This makes your block's "shape" clear to other developers and avoids runtime surprises.
Batching Updates
Lego batches updates using requestAnimationFrame:
this.count = 1;
this.count = 2;
this.count = 3;
// Only one re-render happens!This prevents unnecessary DOM updates and improves performance.
Update Lifecycle
- State Change - You mutate data
- Proxy Intercepts - Change is detected
- Batch Queue - Block added to update queue
- requestAnimationFrame - Browser schedules render
- Re-render - DOM is updated
- updated() Hook - Called after render
{
count: 0,
increment() {
console.log('Before:', this.count);
this.count++;
console.log('After:', this.count);
// DOM not updated yet!
},
updated() {
console.log('DOM updated with:', this.count);
}
}Global Reactivity
Lego also supports Global State available to all blocks and the main document.
// src/main.js
Lego.globals.isLoggedIn = true;For global state to automatically update mustaches in your index.html, you must initialize the engine:
// Enables global reactivity and the MutationObserver
Lego.init();Inside any block, you can access global state via global:
<p b-show="global.isLoggedIn">Welcome!</p>Deep Reactivity
Nested objects are automatically reactive:
{
user: {
profile: {
settings: {
theme: 'dark'
}
}
}
}<!-- All reactive -->
<p>[[ user.profile.settings.theme ]]</p>this.user.profile.settings.theme = 'light'; // ✅ Updates DOMArrays and Objects
Array Mutations
All mutating methods trigger updates:
this.items.push(newItem);
this.items.pop();
this.items.shift();
this.items.unshift(item);
this.items.splice(index, 1);
this.items.sort();
this.items.reverse();Non-Mutating Methods
These don't trigger updates (they return new arrays):
const filtered = this.items.filter(x => x.active); // No update
const mapped = this.items.map(x => x.name); // No updateTo make them reactive, assign back:
this.items = this.items.filter(x => x.active); // ✅ Triggers updateOr use mutating equivalents:
// Instead of filter
for (let i = this.items.length - 1; i >= 0; i--) {
if (!this.items[i].active) {
this.items.splice(i, 1); // ✅ Reactive
}
}Object Changes
Adding Properties to Nested Objects
this.user.newProp = 'value'; // ✅ Reactive (nested object)Object.assign()
Object.assign(this.user, { name: 'Alice', age: 30 }); // ✅ ReactivePerformance Considerations
Multiple Mutations Are Already Batched
LegoDOM's batcher uses a Set to track blocks, so multiple mutations in the same tick result in one re-render:
this.user.name = 'Alice';
this.user.age = 30;
this.user.email = 'alice@example.com';
// ✅ Only one re-render! The batcher deduplicates.Object.assign is fine for readability, but not required for performance:
Object.assign(this.user, {
name: 'Alice',
age: 30,
email: 'alice@example.com'
});
// Also one re-render - same resultConstants Are Fine in State
All properties on state are reactive—but constants never change, so it doesn't matter:
{
count: 0, // Mutable - reactivity matters
MAX_COUNT: 100, // Immutable by convention - reactivity doesn't hurt anything
API_URL: '/api/v1' // Same - just never mutate it
}There's no performance penalty for including constants in your state.
Debugging Reactivity
Check if Value Changed
{
count: 0,
updated() {
console.log('Count changed to:', this.count);
}
}Inspect Proxy
console.log(block.state); // Proxy object
console.log(block.state.count); // Actual valueComparing with Other Frameworks
Vue 3
// Vue 3
const count = ref(0);
count.value++;
// Lego
this.count++;React
// React
const [count, setCount] = useState(0);
setCount(count + 1);
// Lego
this.count++;Svelte
// Svelte
let count = 0;
count++;
// Lego
this.count++;Lego is closest to Svelte's model but uses Proxies instead of compilation.
Advanced Patterns
Watching for Changes
Use updated() hook:
{
count: 0,
previousCount: 0,
updated() {
if (this.count !== this.previousCount) {
console.log('Count changed from', this.previousCount, 'to', this.count);
this.previousCount = this.count;
}
}
}Computed Properties
Use methods:
{
firstName: 'John',
lastName: 'Doe',
fullName() {
return `${this.firstName} ${this.lastName}`;
}
}<p>[[ fullName() ]]</p>Debouncing Updates
{
searchQuery: '',
timer: null,
onInput() {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.performSearch();
}, 300);
}
}Best Practices
- Initialize all properties - Makes your block's shape clear to other developers
- Use array mutating methods -
push(),splice(), etc. trigger updates - Use methods for computed values - They're recalculated on every render
Next Steps
- Learn about Templating
- Explore Directives
- See reactivity examples