Skip to content

Repository files navigation

Observer API – Mutation-Based Reactivity for JavaScript

npm version

bundleLicense


Observe and intercept operations on arbitrary JavaScript objects and arrays using a utility-first, general-purpose reactivity API!

This API re-explores the unique design of the retired Object.observe() API and unifies that with the rest of JavaScript's metaprogramming APIs: Proxies, Reflect, Object!

Observer comes as one little API for all things object observability. (Only ~5.8KiB min|zip)

constobj={};// Observe all property changesObserver.observe(obj,(mutations)=>{mutations.forEach(mutation=>{console.log(`${mutation.type}: ${mutation.key} = ${mutation.value}`);});});Observer.set(obj,'count',5);Observer.deleteProperty(obj,'oldProp');

Tip

Reactivity is anchored on its programmtic APIs like .set(), .deleteProperty(), but you also get reactivity over literal JavaScript operations — obj.prop = value, delete obj.prop, etc. — by means of the accessorize() and proxy() methods covered just ahead.

For full-fledged Imperative Reactive Programming, you want to see the Quantum JS project.


Looking for Observer@1.x?

This documentation is for Observer@2.x. For the previous version, see Observer@1.x.

Table of Contents

Why Observer?

JavaScript is inherently a mutable language but lacks a built-in way to observe said mutations. When you do obj.prop = value or delete obj.prop, there's no mechanism to detect those changes.

The Problem:

conststate={count: 0,items: []};// No way to observe/intercept these mutations in JavaScriptstate.count=5;state.items.push('new item');deletestate.oldProp;// No way to detect these changes

This limitation in the language has long created a blindspot — and a weakness — for reactive systems. Consequently:

  • reactive frameworks (like React, Vue) learned to forbid mutability

  • immutability became the default workaround. You don't mutate, you create a new object each time:

    state={ ...state,count: 6};state={ ...state,count: 7};state={ ...state,count: 8};
    state={ ...state,items: [...state.items,'new item 1']};state={ ...state,items: [...state.items,'new item 2']};state={ ...state,items: [...state.items,'new item 3']};

    Because this is generally hard to follow, frameworks typically enforce immutability by means of strong design constraints.

    Outside of a framework, you get standalone immutability libraries (like Immer, or Immutable.js back in the day) that as well try to simulate an immutable world, where data is never changed, only replaced.

  • mutation gets a bad rap

Using the Observer API:

By enabling observability at the object/array level, the Observer API effectively solves reactivity for a mutable world. The Result is mutation-based reactivity as a first-class concept in JavaScript. Consequently:

  • you are able to weild the full power of mutability in programming to your advantage
  • you are able to make sense of a mutable world — and integrate with it — rather than struggle with it

Quick Start

Install from NPM or include from a CDN.

Installation

npm install @webqit/observer
importObserverfrom'@webqit/observer';

CDN

<scriptsrc="https://unpkg.com/@webqit/observer/dist/main.js"></script><script>constObserver=window.webqit.Observer;</script>

Basic Usage

importObserverfrom'@webqit/observer';constuser={name: 'John',items: []};// Watch for changesconstcontroller=Observer.observe(user,(mutations)=>{mutations.forEach(mutation=>{console.log(`Changed ${mutation.key}: ${mutation.oldValue}${mutation.value}`);});});Observer.set(user,'name','Jane');Observer.set(user,'age',26);// Stop watchingcontroller.abort();

Working with Arrays

constitems=['apple','banana'];Observer.observe(items,(mutations)=>{console.log('Array changed:',mutations);});// Use programmatic APIs for mutationsObserver.set(items,0,'grape');Observer.set(items,2,'orange');// Reactive method callsObserver.apply(items.push,items,['new item']);Observer.proxy(state.items).push('new item')

Intercepting Operations

// Transform values before they're setObserver.intercept(user,'set',(operation,previous,next)=>{if(operation.key==='email'){operation.value=operation.value.toLowerCase();}returnnext();});Observer.set(user,'email','JOHN@EXAMPLE.COM');// Becomes 'john@example.com'

Key Features

Core Reactivity

  • 🔄 Real-time Observability: Watch object and array changes as they happen
  • ⚡ Synchronous Updates: Changes are delivered synchronously, not batched
  • 🎯 Granular Control: Watch specific properties, paths; even wildcards
  • 🌳 Deep Path Watching: Observe nested properties or entire object tree

Advanced Capabilities

  • 🛡️ Operation Interception: Transform, validate, or block operations before execution
  • 🔗 Traps Pipeline: Compose multiple interceptors for complex behavior
  • 📦 Atomic Batching: Batch multiple changes into single atomic operation
  • 🔄 Object Mirroring: Create reactive synchronization between objects

Developer Experience

  • 🔧 Utility-First API: Clean, functional design with consistent patterns
  • 📱 Universal Support: Works in browsers, Node.js, and all JavaScript environments
  • 🔌 Standard Integration: Built on AbortSignal, Reflect API, and Proxy standards
  • 📊 Lightweight: Only ~5.8KB min+gz with zero dependencies

Ecosystem Integrations

The Observer API is enabling a shared protocol for mutation-based reactivity across the ecosystem:

Uses Observer API under the hood to operate as a full-fledged reactive runtime. Quantum enables Imperative Reactive Programming by leveraging Observer's reactivity foundation to make ordinary JavaScript code reactive.

🌐 OOHTML

Uses Observer API to underpin dynamic, reactive UIs. OOHTML enables live data binding between UI and app state, automatically updating the DOM when your data changes.

Webflo

Uses Observer API to underpin Live Objects as a first-class concept. Live Objects in Webflo lets you send dynamic state from your server to the UI with reactivity over the wire.

Uses Observer API to underpin Live Objects as a first-class concept. Live Objects in LinkedQL lets you have query results as self-updating result sets.

API Reference

Observer.observe(target, callback, options?)

Observe changes on an object or array. Returns an AbortController instance for lifecycle management.

Basic Usage:

constobj={};constcontroller=Observer.observe(obj,(mutations)=>{mutations.forEach(mutation=>{console.log(`${mutation.type}: ${mutation.key} = ${mutation.value}`);});});// Changes are delivered synchronouslyObserver.set(obj,'name','Bob');Observer.set(obj,'age',30);// Stop observingcontroller.abort();

Alternative Method Shapes:

// Watch specific propertiesObserver.observe(obj,['name','email'],callback);// Watch a single propertyObserver.observe(obj,'name',callback);// Watch all properties (default)Observer.observe(obj,callback);

Options:

  • signal: A custom AbortSignal instance that control lifecycle
  • diff: Only fire for values that actually changed
  • recursions: Controls recursion handling ('inject', 'force-sync', 'force-async')
  • withPropertyDescriptors: Include property descriptor information

Abort Signals

Observer returns a standard AbortSignal instance for managing observer lifecycle.

// Returns AbortController for lifecycle managementconstcontroller=Observer.observe(obj,callback);controller.abort();// Stop observing// Provide your own AbortSignalconstabortController=newAbortController();Observer.observe(obj,callback,{signal: abortController.signal});abortController.abort();// Stop observing

Or, you can provide your own:

// Providing an AbortSignalconstabortController=newAbortController;Observer.observe(obj,inspect,{signal: abortController.signal});
// Abort at any timeabortController.abort();

Lifecycle Signals

Each lifecycle event fired carries its own Abort Signal that automatically aborts at the end of its turn — just when the next event fires. They're useful for tying other parts of the system to just the given event's lifecycle. For example, lifecycle signals enable parent-child observer relationships where child observers automatically abort when their parent aborts. Leverage this to simplify hierarchical observer patterns.

// Parent observer with lifecycle managementconstparentController=Observer.observe(obj,(mutations,flags)=>{// Child observers automatically abort when parent abortsObserver.observe(obj,childCallback,{signal: flags.signal});// Multiple child observers tied to parent lifecycleObserver.observe(obj,anotherCallback,{signal: flags.signal});});// All child observers abort when parent abortsparentController.abort();

Parity Table

Observer APIObject.observe() (Deprecated)
Signature.observe(target, callback, options?).observe(target, callback, acceptList?)
Return ValueAbortController (lifecycle management)undefined (no lifecycle management)
Additional FeaturesAbortSignal integration, path watching, batch/atomic operations, synchronous event model, etc.Basic object observation, asynchronous event model (deprecated)

Observer.intercept(target, operation, handler, options?)

Intercept operations before they happen. Can intercept individual operations or multiple operations at once.

Single Operation Interception:

Intercept individual operations to transform, validate, or block them before they execute.

// Transform values before they're setObserver.intercept(obj,'set',(operation,previous,next)=>{if(operation.key==='email'){operation.value=operation.value.toLowerCase();}returnnext();});Observer.set(obj,'email','JOHN@EXAMPLE.COM');// Becomes 'john@example.com'

Multiple Operations Interception:

Intercept multiple operations simultaneously to create comprehensive behavior modifications.

constoptions={};Observer.intercept(obj,{set: (operation,previous,next)=>{if(operation.key==='email'){operation.value=operation.value.toLowerCase();}returnnext();},get: (operation,previous,next)=>{if(operation.key==='token'){returnnext(fetchToken());}returnnext();},deleteProperty: (operation,previous,next)=>{if(operation.key==='password'){console.log('Password deletion blocked');returnfalse;// Block the operation}returnnext();}},options);

Traps Pipeline:

Multiple interceptors can intercept same operation, and these will operate like a middleware pipeline where each interceptor uses next() to advance the operation to subsequent interceptors in the pipeline.

// First interceptor: Transform email to lowercaseObserver.intercept(obj,'get',(operation,previous,next)=>{if(operation.key==='email'){constresult=next();returnresult ? result.toLowerCase() : result;}returnnext();});// Second interceptor: Add validationObserver.intercept(obj,'get',(operation,previous,next)=>{if(operation.key==='email'){constresult=next();if(result&&!result.includes('@')){thrownewError('Invalid email format');}returnresult;}returnnext();});// Now when accessing email, both interceptors run in sequence:// 1. First: transforms to lowercase// 2. Second: validates format// Result: 'JOHN@EXAMPLE.COM' → 'john@example.com' → validation passes

Interceptable Operations

  • set - Property assignment
  • get - Property access
  • has - Property existence check
  • ownKeys - Object key enumeration
  • deleteProperty - Property deletion
  • defineProperty - Property definition
  • getOwnPropertyDescriptor - Property descriptor access

Parity Table

Observer APIProxy Traps
Signature.intercept(target, operation, handler, options?)
.intercept(target, { [operation]: handler[, ...]}, options?)
new Proxy(target, { [operation]: handler[, ...] })
Return Valueundefined (registration)Proxy (wrapped object)
Additional FeaturesTraps pipeline, composable interceptorsSingle trap per operation, no composability

Observer.set(target, key, value, options?)

Set properties reactively using a programmatic mutation API. Triggers observers and can be intercepted via Observer.intercept().

Basic Usage:

Observer.set(obj,'name','Alice');Observer.set(arr,0,'first item');

Alternative Method Shapes:

// Set multiple properties at onceObserver.set(obj,{name: 'Alice',age: 25,email: 'alice@example.com'});// Set with receiver contextObserver.set(obj,'name','Alice',receiver);

Usage Patterns

// Reactive state updatesObserver.set(state,'loading',true);Observer.set(state,'data',responseData);// Array operationsObserver.set(items,0,'new item');Observer.set(items,items.length,'append item');// Nested property updatesObserver.set(obj,Observer.path('user','profile','name'),'Alice');

Parity Table

Observer APIReflect API
Signature.set(target, key, value, options?).set(target, key, value)
Return Valueboolean (success)boolean (success)
Additional FeaturesTriggers observers, interceptableStandard property setting

Observer.get(target, key, options?)

Get properties using a programmatic API. Can be intercepted via Observer.intercept() to provide computed values or transformations.

Basic Usage:

constvalue=Observer.get(obj,'name');constnested=Observer.get(obj,'user.profile.name');

Scenario: Computed Properties:

// Intercept to provide computed valuesObserver.intercept(obj,'get',(operation,previous,next)=>{if(operation.key==='fullName'){return`${obj.firstName}${obj.lastName}`;}returnnext();});Observer.get(obj,'fullName');// "John Doe" (computed on-the-fly)

Parity Table

Observer APIReflect API
Signature.get(target, key, options?).get(target, key)
Return Valueany (property value)any (property value)
Additional FeaturesInterceptable for computed valuesStandard property access

Observer.has(target, key, options?)

Check if a property exists on an object. Can be intercepted via Observer.intercept() to hide or reveal properties dynamically.

Basic Usage:

Observer.has(obj,'name');// true/falseObserver.has(obj,'user.profile.name');// nested property check

Scenario: Property Hiding:

// Intercept to hide sensitive propertiesObserver.intercept(obj,'has',(operation,previous,next)=>{if(operation.key==='password'){returnfalse;// Hide password from property checks}returnnext();});Observer.has(obj,'password');// false (hidden from checks)

Parity Table

Observer APIReflect API
Signature.has(target, key, options?).has(target, key)
Return Valueboolean (exists)boolean (exists)
Additional FeaturesInterceptable for property hidingStandard property existence check

Observer.ownKeys(target, options?)

Get all own property keys of an object. Can be intercepted via Observer.intercept() to filter or transform the key list.

Basic Usage:

Observer.ownKeys(obj);// ['name', 'email', 'age']

Scenario: Key Filtering:

// Intercept to filter out sensitive keysObserver.intercept(obj,'ownKeys',(operation,previous,next)=>{constkeys=next();returnkeys.filter(key=>key!=='password');});Observer.ownKeys(obj);// ['name', 'email'] (password filtered out)

Parity Table

Observer APIReflect APIObject API
Signature.ownKeys(target, options?).ownKeys(target).keys(obj)
Return Valuestring[] (keys)string[] (keys)string[] (keys)
Additional FeaturesInterceptable for key filteringStandard key enumerationStandard key enumeration

Observer.deleteProperty(target, key, options?)

Delete properties reactively using a programmatic mutation API.

Basic Usage:

Observer.deleteProperty(obj,'oldProp');Observer.deleteProperty(arr,0);

Parity Table

Observer APIReflect API
Signature.deleteProperty(target, key, options?).deleteProperty(target, key)
Return Valueboolean (success)boolean (success)
Additional FeaturesTriggers observers, interceptableStandard property deletion

Observer.deleteProperties(target, keys, options?)

Delete multiple properties at once.

Observer.deleteProperties(obj,['oldProp1','oldProp2','tempProp']);

Parity Table

Observer APINo Direct Equivalent
Signature.deleteProperties(target, keys, options?)No batch delete in standard APIs
Return Valueboolean[] (success array)N/A
Additional FeaturesTriggers observers, interceptableN/A

Observer.defineProperty(target, key, descriptor, options?)

Define properties reactively using the programmatic mutation API.

Basic Usage:

Observer.defineProperty(obj,'computed',{get: ()=>obj.value*2});

Parity Table

Observer APIReflect APIObject API
Signature.defineProperty(target, key, descriptor, options?).defineProperty(target, key, descriptor).defineProperty(obj, key, descriptor)
Return Valueboolean (success)boolean (success)object (modified object)
Additional FeaturesTriggers observers, interceptableStandard property definitionStandard property definition

Observer.defineProperties(target, descriptors, options?)

Define multiple properties at once.

Observer.defineProperties(obj,{name: {value: 'Alice',writable: true},email: {value: 'alice@example.com',writable: true},age: {value: 25,writable: true}});

Parity Table

Observer APIObject API
Signature.defineProperties(target, descriptors, options?).defineProperties(obj, descriptors)
Return Valueboolean (success)object (modified object)
Additional FeaturesTriggers observers, interceptableStandard property definition

Observer.accessorize(target, properties?, options?)

Make properties reactive for direct assignment.

constobj={age: null};// Make all CURRENT properties reactiveObserver.accessorize(obj);// Make specific properties reactiveObserver.accessorize(obj,['name','email']);// Now direct assignment worksobj.name='Alice';obj.email='alice@example.com';

Parity Table

Observer APINo Direct Equivalent
Signature.accessorize(target, properties?, options?)No direct equivalent in standard APIs
Return Valueundefined (modification)N/A
Additional FeaturesMakes properties reactive for direct assignmentN/A

Observer.unaccessorize(target, properties?)

Restore accessorized properties to their normal state.

// Restore specific propertiesObserver.unaccessorize(obj,['name','email'],options?);// Restore all accessorized propertiesObserver.unaccessorize(obj);

Parity Table

Observer APINo Direct Equivalent
Signature.unaccessorize(target, properties?, options?)No direct equivalent in standard APIs
Return Valueundefined (modification)N/A
Additional FeaturesRestores accessorized properties to normal stateN/A

Observer.proxy(target, options?)

Create a reactive proxy of any object to get automatic reactivity and interceptibility over on-the-fly operations.

Basic Usage:

const$obj=Observer.proxy(obj);// All operations are reactive$obj.name='Alice';// Triggers observers$obj.newProp='value';// Triggers observersdelete$obj.oldProp;// Triggers observers// Array methods are reactive$arr.push('item1','item2');// Triggers observers$arr[0]='newValue';// Triggers observers

Nested Operations (Requires chainable: true)

Use chainable: true to interact with deeply nested objects as proxy instances too. By default, .proxy() doesn't perform deep wrapping - nested objects are returned as plain objects. Chainable mode enables automatic proxying of nested objects at the point they're accessed, allowing nested operations to trigger observers.

const$obj=Observer.proxy(obj,{chainable: true});// Nested objects are automatically proxied when accessedconst$user=$obj.user;// Returns a proxied object$user.name='Alice';// Triggers observers$user.profile.theme='dark';// Triggers observers// Array methods return proxied arraysconst$filtered=$obj.items.filter(x=>x.active);// Returns proxied array$filtered.push('newItem');// Triggers observers// Direct nested access also works$obj.users[0].name='Bob';// Triggers observers$obj.data.splice(0,1);// Triggers observers

Membrane Mode

Membranes ensure that the same proxy instance is returned across multiple .proxy() calls for the same object. When combined with chainable: true, membranes also ensure consistent proxy identity for nested objects.

// Create membrane for consistent proxy identityconst$obj1=Observer.proxy(obj,{membrane: 'userData'});const$obj2=Observer.proxy(obj,{membrane: 'userData'});// Same proxy instance returnedconsole.log($obj1===$obj2);// true// Root operations are reactive$obj1.name='Alice';// Triggers observers$obj2.email='alice@example.com';// Triggers observers (same proxy)// When combined with chainable: trueconst$obj3=Observer.proxy(obj,{membrane: 'userData',chainable: true});const$user1=$obj3.user;const$user2=$obj3.user;// Same nested proxy instance returnedconsole.log($user1===$user2);// true$user1.name='Alice';// Triggers observers

How Membranes Work

  • Root Object Identity - Same root object always returns the same proxy instance across multiple .proxy() calls
  • Membrane References - Uses a reference system to ensure consistent proxy identity
  • Nested Object Identity - When combined with chainable: true, ensures same nested objects return same proxy instances
  • Performance - Only creates one proxy per object (root or nested)
  • Consistency - Maintains referential equality for both root and nested objects

Membrane vs Chainable Object Identity

constobj={user: {name: 'Alice'},items: ['item1']};// MEMBRANE: Same root object = same proxy instanceconst$obj1=Observer.proxy(obj,{membrane: 'test'});const$obj2=Observer.proxy(obj,{membrane: 'test'});console.log($obj1===$obj2);// true - same root proxy// Nested objects are NOT automatically proxied (without chainable)constuser1=$obj1.user;// Plain object, not proxiedconstuser2=$obj2.user;// Plain object, not proxiedconsole.log(user1===user2);// true - same plain object// CHAINABLE: Auto-proxies nested objects when accessedconst$obj=Observer.proxy(obj,{chainable: true});const$user1=$obj.user;// Proxied objectconst$user2=$obj.user;// Different proxy instanceconsole.log($user1!==$user2);// true - different proxy instances// MEMBRANE + CHAINABLE: Consistent nested proxy identityconst$obj3=Observer.proxy(obj,{membrane: 'test',chainable: true});const$user3=$obj3.user;// Proxied objectconst$user4=$obj3.user;// Same proxy instanceconsole.log($user3===$user4);// true - same nested proxy

Real-World Usage Patterns

Scenario: Form Handling:

const$form=Observer.proxy(formData,{membrane: 'form'});$form.name='John';// Auto-save, validation$form.email='john@example.com';// Auto-save, validation$form.tags.push('urgent');// Auto-save, validation

Scenario: State Management:

const$state=Observer.proxy(appState,{chainable: true});$state.user.isLoggedIn=true;// UI updates$state.cart.items.push(product);// UI updates$state.getUser().profile.theme='dark';// UI updates (chainable)

Formal Arguments

// Basic proxyconst$obj=Observer.proxy(obj);// Proxy with optionsconst$obj=Observer.proxy(obj,{membrane: 'userData',// Auto-proxy nested objectschainable: true// Auto-wrap returned objects});// Proxy with custom extensionconst$obj=Observer.proxy(obj,{},(traps)=>{// Extend proxy trapstraps.get=(target,key,receiver)=>{if(key==='computed'){returntarget.firstName+' '+target.lastName;}returntraps.get(target,key,receiver);};returntraps;});

Proxy Features (Summary)

  • Literal syntax - Use normal JavaScript operations
  • Array methods - All array methods are reactive
  • Property access - All property operations are reactive
  • Nested operations - Works with deeply nested objects
  • Dynamic properties - Supports computed property names
  • Method chaining - Array methods can be chained
  • Membrane support - Auto-proxy nested objects
  • Chainable operations - Auto-wrap returned values
  • Custom traps - Extend proxy behavior
  • Namespace isolation - Separate observer namespaces

Parity Table

Observer APIProxy API
Signature.proxy(target, options?)new Proxy(target, handlers)
Return ValueProxy (reactive proxy)Proxy (standard proxy)
Additional FeaturesBuilt-in reactivity, membrane, chainableManual trap implementation required

Observer.unproxy(target, options?)

Get the original object from a proxy.

const$obj=Observer.proxy(obj);constoriginal=Observer.unproxy($obj);// Returns original obj

Parity Table

Observer APINo Direct Equivalent
Signature.unproxy(target)No direct equivalent in standard APIs
Return Valueobject (original object)N/A
Additional FeaturesExtracts original object from Observer proxyN/A

Observer.path(...segments)

Create path arrays for deep property observation. Path watching enables observing changes at specific nested paths within object trees, including non-existent paths that are created dynamically.

Basic Usage:

// Watch deep pathsconstpath=Observer.path('user','profile','settings');Observer.observe(obj,path,(mutation)=>{console.log(`Deep change: ${mutation.path} = ${mutation.value}`);});

Usage Patterns

// Form validationconstpath=Observer.path('form','user','email');Observer.observe(form,path,(mutation)=>{validateEmail(mutation.value);});// State managementconstpath=Observer.path('app','user','preferences','theme');Observer.observe(state,path,(mutation)=>{updateTheme(mutation.value);});// Configuration watchingconstpath=Observer.path('config','api','endpoint');Observer.observe(config,path,(mutation)=>{updateApiEndpoint(mutation.value);});

Path Features (Summary)

  • Watches paths that are created dynamically
  • Uses an array syntax to avoid conflicting with property names with dots
  • Returns mutation context for audit trails

Observer.any()

Create a wildcard directive for matching any property or array index in path patterns. Wildcards enable flexible observation of dynamic data structures where you need to watch changes at any index or property name.

Basic Usage:

// Watch any user at any indexconstpath=Observer.path('users',Observer.any(),'name');Observer.observe(obj,path,(mutation)=>{console.log(`User name changed: ${mutation.path} = ${mutation.value}`);});

Advanced Compositions:

Combine multiple wildcards to create powerful observation patterns for complex data structures. This enables watching changes across dynamic arrays, nested objects, and varying property names.

// Multiple wildcards in sequenceconstpath=Observer.path('sections',Observer.any(),'items',Observer.any(),'name');// Matches: sections[0].items[1].name, sections[2].items[0].name, etc.// Wildcard at different levelsconstpath=Observer.path('app','users',Observer.any(),'profile','settings',Observer.any());// Matches: app.users[0].profile.settings[theme], app.users[1].profile.settings[language], etc.// Wildcard with specific propertiesconstpath=Observer.path('data',Observer.any(),'metadata','version');// Matches: data[item1].metadata.version, data[item2].metadata.version, etc.

Observer.subtree()

Create a subtree directive for watching all changes from a specific level down infinitely. Subtree watching enables comprehensive observation of complex nested data structures without needing to specify every possible path.

Basic Usage:

// Watch all changes from this level downObserver.observe(obj,Observer.subtree(),(mutation)=>{console.log(`Any change: ${mutation.path} = ${mutation.value}`);});

Advanced Compositions:

// Subtree after specific pathconstpath=Observer.path('app','users',Observer.subtree());// Watches: app.users.name, app.users.profile.theme, app.users.settings.notifications, etc.// Subtree with wildcardsconstpath=Observer.path('sections',Observer.any(),Observer.subtree());// Watches: sections[0].title, sections[0].items[1].name, sections[1].config.theme, etc.// Multiple subtreesconstpath=Observer.path('app','users',Observer.any(),'profile',Observer.subtree());// Watches: app.users[0].profile.name, app.users[0].profile.settings.theme, etc.// Subtree at root levelObserver.observe(obj,Observer.subtree(),(mutation)=>{// Watches EVERY change in the entire object tree});

Real-World Usage Patterns

// E-commerce: Watch any product in any categoryconstpath=Observer.path('store','categories',Observer.any(),'products',Observer.any(),Observer.subtree());// Triggers for: store.categories[electronics].products[laptop].price// store.categories[books].products[novel].title// store.categories[clothing].products[shirt].sizes[large]
// Multi-tenant: Watch any user's data in any organizationconstpath=Observer.path('orgs',Observer.any(),'users',Observer.any(),Observer.subtree());// Triggers for: orgs[company1].users[alice].profile.name// orgs[company2].users[bob].settings.theme
// Content Management: Watch any page in any sectionconstpath=Observer.path('cms','sections',Observer.any(),'pages',Observer.any(),Observer.subtree());// Triggers for: cms.sections[blog].pages[post1].content// cms.sections[news].pages[article].metadata.tags

Observer.batch(target, callback, options?)

Batch multiple operations together. Batched operations ensure atomicity - all changes are delivered as a single event to observers, preventing partial updates and ensuring data consistency.

Basic Usage:

// Batch multiple changesObserver.batch(obj,()=>{Observer.set(obj,'name','Alice');Observer.set(obj,'email','alice@example.com');Observer.deleteProperty(obj,'age');});// All changes are delivered as a single batch to observers

Observer.map(source, target, options?)

Create reactive mirrors between objects — changes in source automatically sync to target. Object mirroring enables automatic data flow between different parts of your application, keeping them synchronized without manual intervention.

Basic Usage:

constsource={name: 'Alice',age: 25};consttarget={};// Create reactive mirrorconstcontroller=Observer.map(source,target);// Changes in source automatically sync to targetObserver.set(source,'name','Bob');console.log(target.name);// 'Bob'// Stop mirroringcontroller.abort();

Alternative Method Shapes:

// Mirror with optionsObserver.map(source,target,{only: ['name','email'],// Only mirror specific propertiesexcept: ['password'],// Exclude specific propertiesspread: true,// Spread array elementsonlyEnumerable: false// Include non-enumerable properties});// Mirror with namespaceObserver.map(source,target,{namespace: 'user'});

Usage Patterns

// State synchronizationconstappState={user: {name: 'Alice'}};constuiState={};Observer.map(appState,uiState);// Form data mirroringconstformData={name: '',email: ''};constvalidationState={};Observer.map(formData,validationState);// Array synchronizationconstsourceArray=[1,2,3];consttargetArray=[];Observer.map(sourceArray,targetArray,{spread: true});

Other Methods

Mentioned here for completeness, Observer also provides these utility methods:

  • Observer.apply(target, thisArg, args) - Apply functions reactively
  • Observer.construct(target, args) - Construct objects reactively
  • Observer.getOwnPropertyDescriptor(target, key) - Get property descriptors reactively
  • Observer.getPrototypeOf(target) - Get prototype reactively
  • Observer.setPrototypeOf(target, prototype) - Set prototype reactively
  • Observer.isExtensible(target) - Check extensibility reactively
  • Observer.preventExtensions(target) - Prevent extensions reactively

Extended Documentation

Contributing

We welcome contributions! Here's how to get involved:

License

MIT

About

Mutation-Based Reactivity for JavaScript

Topics

Resources

Stars

23 stars

Watchers

2 watching

Forks

Sponsor this project

Used by

Contributors

Languages