PatternsArchitecture Patterns
Proxy Pattern
Allows intercepting and customizing operations on objects - foundation of libraries like MobX or ref functionality in Vue/React
The Proxy Pattern provides a substitute or placeholder for another object to control access to it. It allows intercepting and customizing operations on objects. It's the foundation of libraries like MobX or the ref functionality in Vue/React.
🎯 The Concept
// Proxy intercepts operations on objects
const target = { name: 'John', age: 30 };
const proxy = new Proxy(target, {
get(target, prop) {
console.log(`Accessing ${String(prop)}`);
return target[prop];
},
set(target, prop, value) {
console.log(`Setting ${String(prop)} = ${value}`);
target[prop] = value;
return true;
}
});
proxy.name; // Log: "Accessing name"
proxy.age = 31; // Log: "Setting age = 31"📚 Common Front-End Examples
1. Proxy for Object Validation
function createValidatedProxy<T extends object>(
target: T,
validators: Record<string, (value: unknown) => boolean>
) {
return new Proxy(target, {
set(obj, prop, value) {
const validator = validators[String(prop)];
if (validator && !validator(value)) {
throw new Error(`Validation failed for ${String(prop)}`);
}
obj[prop] = value;
return true;
}
});
}
// Usage
const user = createValidatedProxy(
{ name: '', email: '' },
{
name: (value) => value.length >= 3,
email: (value) => value.includes('@')
}
);
user.name = 'Jo'; // ❌ Error: validation failed
user.name = 'John'; // ✅ OK
user.email = 'invalid'; // ❌ Error: validation failed
user.email = 'john@example.com'; // ✅ OK2. Proxy for Observability (MobX-like)
type Listener = () => void;
function createObservable<T extends object>(target: T) {
const listeners = new Set<Listener>();
const proxy = new Proxy(target, {
set(obj, prop, value) {
const changed = obj[prop] !== value;
obj[prop] = value;
if (changed) {
// Notify listeners when value changes
listeners.forEach(listener => listener());
}
return true;
}
});
return {
proxy,
subscribe(listener: Listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
};
}
// Usage
const { proxy: state, subscribe } = createObservable({ count: 0 });
subscribe(() => {
console.log('State changed:', state);
});
state.count = 1; // Log: "State changed: { count: 1 }"
state.count = 2; // Log: "State changed: { count: 2 }"3. Proxy for Request Caching
import { useState, useEffect } from 'react';
function createCachedApi<T extends Record<string, (...args: unknown[]) => Promise<unknown>>>(api: T) {
const cache = new Map<string, unknown>();
return new Proxy(api, {
get(target, prop) {
const originalMethod = target[prop];
if (typeof originalMethod === 'function') {
return async (...args: unknown[]) => {
const cacheKey = `${String(prop)}-${JSON.stringify(args)}`;
if (cache.has(cacheKey)) {
console.log('Cache hit:', cacheKey);
return cache.get(cacheKey);
}
console.log('Cache miss:', cacheKey);
const result = await originalMethod.apply(target, args);
cache.set(cacheKey, result);
return result;
};
}
return originalMethod;
}
});
}
// Usage
interface User {
id: number;
name: string;
email: string;
}
const api = {
async getUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json() as Promise<User>;
},
async getUsers(): Promise<User[]> {
const res = await fetch('/api/users');
return res.json() as Promise<User[]>;
}
};
const cachedApi = createCachedApi(api);
async function fetchUserData() {
// First call: fetch from server
const user1 = await cachedApi.getUser(1) as User;
console.log('Fetched user:', user1.name);
// Second call: return from cache
const user1Cached = await cachedApi.getUser(1) as User; // Cache hit!
console.log('Cached user:', user1Cached.name);
// Different user: fetch from server
const user2 = await cachedApi.getUser(2) as User;
console.log('Fetched user 2:', user2.name);
}
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
(async () => {
const userData = await cachedApi.getUser(userId) as User;
setUser(userData);
})();
}, [userId]);
if (!user) return <div>Loading...</div>;
return <div>{user.name} - {user.email}</div>;
}
### 4. Proxy for Operation Logging
```tsx
function createLoggedProxy<T extends object>(
target: T,
name: string = 'Object'
) {
return new Proxy(target, {
get(obj, prop) {
const value = obj[prop];
console.log(`[${name}] GET ${String(prop)}`, value);
return value;
},
set(obj, prop, value) {
console.log(`[${name}] SET ${String(prop)} =`, value);
obj[prop] = value;
return true;
},
has(obj, prop) {
const has = prop in obj;
console.log(`[${name}] HAS ${String(prop)}`, has);
return has;
},
deleteProperty(obj, prop) {
console.log(`[${name}] DELETE ${String(prop)}`);
delete obj[prop];
return true;
}
});
}
// Usage
const user = createLoggedProxy(
{ name: 'John', age: 30 },
'User'
);
user.name; // Log: "[User] GET name John"
user.age = 31; // Log: "[User] SET age = 31"
'name' in user; // Log: "[User] HAS name true"
delete user.age; // Log: "[User] DELETE age"5. Proxy for Lazy Loading
function createLazyProxy<T extends object>(
loader: () => Promise<T>
) {
let target: T | null = null;
let loading = false;
let promise: Promise<T> | null = null;
return new Proxy({} as T, {
get(obj, prop) {
if (target) {
return target[prop];
}
if (!promise) {
loading = true;
promise = loader().then(loaded => {
target = loaded;
loading = false;
return loaded;
});
}
// Return promise if still loading
if (loading) {
return promise.then(loaded => loaded[prop]);
}
return target?.[prop];
}
});
}
// Usage
const lazyUser = createLazyProxy(async () => {
const res = await fetch('/api/user');
return res.json();
});
// First access: load data
const name = await lazyUser.name; // Fetch from server
// Subsequent accesses: return from cache
const email = lazyUser.email; // Already loaded🎯 When to Use
This pattern is commonly used and recommended for:
- Data validation - Intercept set to validate before assigning
- Observability - Detect changes in objects (MobX, Vue reactivity)
- Request caching - Intercept API calls to cache results
- Logging and debugging - Log all operations on objects
- Lazy loading - Load data only when accessed for the first time
🔗 Relationship with React/Vue
React ref
// React uses Proxy internally for refs
const inputRef = useRef<HTMLInputElement>(null);
// Proxy allows accessing DOM properties
inputRef.current?.focus();Vue Reactivity
// Vue 3 uses Proxy for reactivity
const state = reactive({ count: 0 });
// Proxy detects changes automatically
state.count = 1; // Vue detects and updates UI⚠️ Considerations
- Performance - Proxy adds overhead, use in moderation
- Compatibility - Doesn't work in very old browsers (IE)
- Debugging - Can make debugging harder (stack traces)
- TypeScript - Can be difficult to type correctly
📚 Key Takeaways
- Intercepts operations - get, set, has, delete, etc.
- Transparent - Client code doesn't know it's using proxy
- Flexible - Can add behavior without modifying original object
- Foundation of reactivity - MobX, Vue 3 use Proxy
- Use carefully - Can impact performance if used excessively