Skip to content

hx data Core Concepts

github-actions[bot] edited this page Jun 18, 2026 · 1 revision

Core Concepts

Reactive Objects

A reactive object is a JavaScript object wrapped in a Proxy. Any property read/write is intercepted and tracked, enabling automatic change detection.

import { reactive } from '@hx/data';

const root = reactive({
  profile: {
    name: 'Alice',
    age: 30
  },
  tags: ['admin', 'editor']
});

Root vs Nested

  • ReactiveRoot — the top-level object created by reactive(). Has the full API: event triggers, change listeners.
  • ReactiveObject — any nested object or array. Delegates operations to its root.
const root = reactive({ user: { name: 'John' } });
const user = root.user;        // ReactiveObject (nested)
const name = user.name;        // plain string (primitive)

ERO.isReactiveRoot(root);      // true
ERO.isReactiveRoot(user);      // false
ERO.isReactiveObject(user);    // true
ERO.isReactiveObject(name);    // false — primitives aren't proxied

Revocation

Unwrap a reactive object back to plain JavaScript:

const reactiveObj = reactive({ x: 1, y: 2 });
const plain = ERO.revoke(reactiveObj);
// plain === { x: 1, y: 2 } (no proxy, no events)

Change Events

Every modification fires a ValueChangedEvent with full context:

interface ValueChangedEvent {
  root: ReactiveRoot;         // the root reactive object
  parent: ReactiveObject;     // direct parent of the changed property
  oldValue: any;              // value before change
  newValue: any;              // value after change
  pathToRoot: PathToRoot;     // absolute path from root (e.g. "user.address.city")
  pathToParent: PathToParent; // path from parent (e.g. "city" or "[0]")
}

Path Matching Rules

Three listening modes:

Pattern Matches
* Everything in the entire reactive tree
path.* Changes at path OR any descendant
path Exact match only
const obj = reactive({ user: { name: 'John', age: 30 } });

ERO.on(obj, '*', (e) => console.log('global:', e.pathToRoot));
ERO.on(obj, 'user.*', (e) => console.log('user subtree:', e.pathToRoot));
ERO.on(obj, 'user.name', (e) => console.log('exact name:', e.pathToRoot));

obj.user.name = 'Jane';  // all three fire
obj.user.age = 31;       // '*' and 'user.*' fire, 'user.name' does NOT

Array Changes

Array mutations (push, pop, shift, unshift, splice, sort, reverse, fill, copyWithin) are automatically detected and fire appropriate events:

const obj = reactive({ items: [1, 2, 3] });

ERO.on(obj, 'items.*', (e) => {
  console.log(e.oldValue, '→', e.newValue);
});

obj.items.push(4);  // fires: [1,2,3] → [1,2,3,4]
obj.items[0] = 10;  // fires: 1 → 10

Array mutation methods (push, pop, shift, unshift, splice) trigger ONE change event (old array copy → new array copy). The duplicate length-change event is suppressed internally.

ValueChangedEvent in Detail

const root = reactive({ user: { address: { city: 'NYC' } } });

ERO.on(root, 'user.address.city', (event) => {
  event.root           // === root
  event.parent         // === root.user.address
  event.oldValue       // 'NYC'
  event.newValue       // 'Boston'
  event.pathToRoot     // 'user.address.city'
  event.pathToParent   // 'city'
});

root.user.address.city = 'Boston';

Silence Modes

Control whether change events fire:

type ValueSetSilenceMode = 'loud' | 'mute-all' | 'mute-leaf';
Mode Behavior
loud Default — all events fire normally
mute-all No events fire for the entire operation
mute-leaf Only the final write is silent; intermediate path creation still fires
const obj = reactive({ user: { name: 'John' } });

// Normal: fires event
ERO.setValue(obj, 'user.name', 'Jane');

// Silent: no event
ERO.setValueSilent(obj, 'user.name', 'Jane', 'mute-all');

Clone this wiki locally