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 changeProxy 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 implementationAdvanced 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 rulesImplementation 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:
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';State changed: name = "Jane Doe"
State changed: loading = true
State changed: theme = "light"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());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: trueconst 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'));Initial computed: { fullName: 'John Doe', age: 34, initials: 'JD' }
After name change:
Full name: Jane Smith
Initials: JS
Age: 34Concepts
Complexity Analysis
Implementation
basic-observable
// 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"