Skip to main content

Proxy-Based Observables

Create reactive objects using ES6 Proxy for automatic change detection

Pattern Overview

🔍 Proxy-Based Observables Pattern

The Proxy-Based Observables Pattern leverages ES6 Proxy to create reactive objects that automatically notify observers when properties change. This enables building reactive systems with minimal boilerplate.

Core Concepts

🔹 ES6 Proxy - Intercepts and customizes operations on objects (property access, assignment)
🔹 Automatic Detection - Changes are detected without explicit setter methods
🔹 Deep Observation - Nested objects can be recursively observed
🔹 Transparent Usage - Objects behave normally while being observed

Real-World Applications

State Management - Redux-like stores with automatic change detection Form Validation - Real-time validation as user types Data Binding - Two-way binding between models and views Computed Properties - Auto-recalculation when dependencies change

Proxy Advantages Over Traditional Observables

No Manual Setup - Properties are automatically observable without decoration Dynamic Properties - New properties added at runtime are automatically observed Minimal Code - No need for getter/setter pairs or observable decorators Native Performance - Browser-optimized Proxy implementation

Advanced Features

Debouncing - Batch rapid changes to prevent excessive notifications Deep Observation - Automatically observe nested objects and arrays Change History - Track property changes over time Validation Integration - Combine observation with validation rules

Implementation Benefits

Automatic reactivity - No manual observer setup required
Natural syntax - Regular property access and assignment
Deep observation - Nested objects automatically wrapped
Performance optimized - Native Proxy implementation in modern browsers

Examples:
Create application state that automatically notifies when properties change
Input:
const initialState: AppState = {
  user: { id: 1, name: 'John', email: 'john@example.com', preferences: { theme: 'dark', language: 'en', notifications: true } },
  ui: { loading: false, errors: [], currentPage: 'home' },
  data: { items: [], filters: {}, pagination: { page: 1, pageSize: 10, total: 0 } }
};

const stateManager = createStateManager(initialState);
const state = stateManager.getState();

// Add change listener
stateManager.addStateListener((target, property, value) => {
  console.log(`State changed: ${String(property)} = ${JSON.stringify(value)}`);
});

// Make changes
state.user.name = 'Jane Doe';
state.ui.loading = true;
state.user.preferences.theme = 'light';
Output:
State changed: name = "Jane Doe"
State changed: loading = true  
State changed: theme = "light"
Create reactive form with automatic validation as user types
Input:
const form = createReactiveForm();

// Setup form fields
form.addField('email', '', true);
form.addField('password', '', true);

// Add validation rules
form.addValidationRule('email', (value) => {
  return value.includes('@') ? null : 'Invalid email format';
});

form.addValidationRule('password', (value) => {
  return value.length >= 6 ? null : 'Password must be at least 6 characters';
});

// Simulate user input
form.setValue('email', 'john');
console.log('Email field:', form.getField('email'));

form.setValue('email', 'john@example.com');
form.setValue('password', '12345');
console.log('Form valid:', form.isValid());

form.setValue('password', '123456');
console.log('Form valid after password fix:', form.isValid());
Output:
Form validation errors: { email: 'Invalid email format' }
Email field: { value: 'john', error: 'Invalid email format', touched: true, required: true }
Form validation errors: { password: 'Password must be at least 6 characters' }
Form valid: false
Form valid after password fix: true
Create reactive model where computed properties automatically update when dependencies change
Input:
const userData: UserModel = {
  firstName: 'John',
  lastName: 'Doe', 
  email: 'john@example.com',
  birthDate: new Date('1990-05-15'),
  preferences: { theme: 'light', language: 'en' }
};

const userModel = createUserModel(userData);

console.log('Initial computed:', userModel.getAllComputed());

// Change name - computed properties auto-update
userModel.data.firstName = 'Jane';
userModel.data.lastName = 'Smith';

console.log('After name change:');
console.log('Full name:', userModel.getComputed('fullName'));
console.log('Initials:', userModel.getComputed('initials'));
console.log('Age:', userModel.getComputed('age'));
Output:
Initial computed: { fullName: 'John Doe', age: 34, initials: 'JD' }
After name change:
Full name: Jane Smith
Initials: JS
Age: 34

Concepts

design patternssoftware architecturecode organizationobject-oriented programming

Complexity Analysis

Time:O(1) for property access, O(n) for deep observation
Space:O(n) where n is the number of observers

Implementation

basic-observable

Time: O(1) | Space: O(n)
// Basic Observable using Proxy
type ChangeListener<T> = (target: T, property: keyof T, value: any, oldValue: any) => void;

function createObservable<T extends object>(
  target: T,
  listeners: Array<ChangeListener<T>> = []
): T {
  return new Proxy(target, {
    set(obj, property, value) {
      const oldValue = (obj as any)[property];
      (obj as any)[property] = value;
      
      // Notify listeners
      listeners.forEach(listener => {
        try {
          listener(obj as T, property as keyof T, value, oldValue);
        } catch (error) {
          console.error('Observer error:', error);
        }
      });
      
      return true;
    }
  });
}

// Usage
const data = createObservable({ count: 0, name: 'test' }, [
  (target, property, value) => console.log(`${String(property)} changed to ${value}`)
]);

data.count = 5; // Logs: "count changed to 5"
data.name = 'updated'; // Logs: "name changed to updated"