Skip to content
DevinimJS
Phase 1 · Lesson 4

Reactivity — this.state and Batched Rendering

Mutate this.state directly. DevinimJS detects changes, batches them, and updates the DOM in one efficient pass.

What you'll learn
  • How JavaScript Proxy makes this.state reactive
  • 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

Live demo — watch batched updates

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.