- How JavaScript Proxy makes
this.statereactive - Mutate state directly — no
setState(), no immutability requirement - Batched rendering: multiple mutations = one DOM update
- Using
requestUpdate()for non-state triggers - Nested reactivity with objects and arrays
Direct mutation, not setState()
In DevinimJS you mutate this.state directly. There is no setState(), no useState(), no dispatch function:
increment() {
this.state.count += 1; // ← direct mutation. That's it.
}
reset() {
this.state.count = 0;
this.state.lastReset = new Date().toISOString();
}
The Proxy traps the assignment and schedules a re-render on the next microtask.
Batched rendering
Multiple state mutations in the same synchronous block trigger one re-render:
shuffle() {
this.state.score += 10;
this.state.round += 1;
this.state.message = `Round ${this.state.round} complete`;
// All three mutations → one template() call on the next microtask
}
This is efficient and predictable. You don't need to batch manually — the framework does it.
Deep reactivity
Nested objects and arrays are also reactive. The Proxy wraps deeply:
initialState() {
return {
user: { name: 'Guest', email: '' },
items: []
};
}
updateName() {
this.state.user.name = 'Alice'; // ← deep mutation detected
}
addItem(product) {
this.state.items.push(product); // ← array mutation detected
}
Dot-path tracking
The updated(changedKeys) hook receives dot-paths like ['user.name'] or ['items.0']. You can react to specific nested changes.
requestUpdate() — the escape hatch
Sometimes you need a re-render without mutating state. Use requestUpdate() for external triggers:
connected() {
// Timers, WebSocket messages, external DOM events
this.listen(window, 'resize', () => this.requestUpdate());
this.listen(someExternalSource, 'data', () => this.requestUpdate());
}
After requestUpdate(), template() runs again but updated() is not called (since no state keys changed).
How the Proxy works (briefly)
createReactive(target, onChange) wraps your object in a Proxy:
import { createReactive } from '.../core.min.js';
const data = createReactive({ count: 0, items: [] }, (path) => {
console.log('Changed:', path); // e.g. "count", "items.0"
});
data.count += 1; // logs "Changed: count"
data.items.push('a'); // logs "Changed: items" and "Changed: items.0"
BaseComponent calls createReactive() internally. The onChange callback schedules template() for the next microtask.
Things that pass through un-wrapped
Class instances like Date, Map, Set, and non-plain objects are not wrapped by the Proxy. They pass through as-is:
initialState() {
return {
since: new Date(), // ← Date is not proxied
tags: new Set(['ui']), // ← Set is not proxied
cache: new Map(), // ← Map is not proxied
};
}
If you need reactivity on these values, replace them entirely: this.state.since = new Date().
Live demo: Batched mutations
Click "Multi-step update" to see 3 state mutations in one re-render.
Recap
Mutate directly
this.state.x = y. No setter function, no immutability.
Batched automatically
Multiple sync mutations → one template() call on next microtask.
Deeply reactive
Nested objects and arrays are tracked. Dot-path in updated().
Escape hatch
requestUpdate() for external triggers that aren't state mutations.