JavaScript Proxy has been in the language since ES2015 and most developers have never used it for anything practical. It intercepts fundamental object operations — property access, assignment, deletion, function calls — and lets you add behaviour transparently. Vue 3’s reactivity system is built on Proxy. MobX uses it. Immer uses it. Here’s why they did.
⚡ TL;DR:
new Proxy(target, handler)wraps an object and intercepts operations via handler traps.get,set,has,deleteProperty, andapplyare the five traps that cover 90% of use cases. Each returns a transparent wrapper — callers never know they’re talking to a Proxy.
Use Case 1: Runtime Validation
function createValidated(schema) {
return new Proxy({}, {
set(target, prop, value) {
const validator = schema[prop];
if (!validator) throw new Error(`Unknown property: ${prop}`);
if (!validator(value)) throw new TypeError(`Invalid value for ${prop}: ${value}`);
target[prop] = value;
return true;
}
});
}
const user = createValidated({
name: (v) => typeof v === 'string' && v.length > 0,
age: (v) => Number.isInteger(v) && v >= 0 && v <= 150,
email: (v) => /^[^@]+@[^@]+\.[^@]+$/.test(v),
});
user.name = 'Alice'; // OK
user.age = 30; // OK
user.age = -5; // TypeError: Invalid value for age
user.role = 'admin'; // Error: Unknown property: role
Use Case 2: Observable State (Vue 3 Pattern)
function observable(obj, onChange) {
return new Proxy(obj, {
set(target, prop, value) {
const old = target[prop];
target[prop] = value;
if (old !== value) onChange(prop, old, value);
return true;
},
deleteProperty(target, prop) {
const old = target[prop];
delete target[prop];
onChange(prop, old, undefined);
return true;
}
});
}
const state = observable({ count: 0, name: 'Alice' }, (prop, old, next) => {
console.log(`${prop}: ${old} -> ${next}`);
document.getElementById(prop)?.textContent = next; // Auto-update DOM
});
state.count++; // count: 0 -> 1
state.name = 'Bob'; // name: Alice -> Bob
// No framework needed for simple reactive state
Use Case 3: Memoization Proxy
function memoize(fn) {
const cache = new Map();
return new Proxy(fn, {
apply(target, thisArg, args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Cache hit', key);
return cache.get(key);
}
const result = target.apply(thisArg, args);
cache.set(key, result);
return result;
}
});
}
const expensiveFn = memoize((n) => {
// Simulate heavy computation
return Array.from({length: n}, (_, i) => i).reduce((a, b) => a + b, 0);
});
expensiveFn(1000); // Computed
expensiveFn(1000); // Cache hit — instant
Use Case 4: Negative Array Indexing (Python-Style)
function negativeArray(arr) {
return new Proxy(arr, {
get(target, prop) {
const index = Number(prop);
if (!isNaN(index) && index < 0) {
return target[target.length + index]; // -1 = last, -2 = second-to-last
}
return Reflect.get(target, prop);
}
});
}
const arr = negativeArray([1, 2, 3, 4, 5]);
arr[-1]; // 5 (last)
arr[-2]; // 4
arr[0]; // 1 (positive indices still work)
arr.length; // 5 (all other operations work normally)
Use Case 5: Auto-Logging / Tracing
function trace(obj, label = 'object') {
return new Proxy(obj, {
get(target, prop) {
const val = target[prop];
if (typeof val === 'function') {
return function(...args) {
console.log(`[${label}] ${String(prop)}(${args.map(a => JSON.stringify(a)).join(', ')})`);
const result = val.apply(target, args);
console.log(`[${label}] ${String(prop)} returned:`, result);
return result;
};
}
console.log(`[${label}] get ${String(prop)} =`, val);
return val;
},
set(target, prop, value) {
console.log(`[${label}] set ${String(prop)} =`, value);
target[prop] = value;
return true;
}
});
}
const api = trace({ users: [], addUser: (u) => api.users.push(u) }, 'UserAPI');
api.addUser({ name: 'Alice' }); // [UserAPI] addUser({"name":"Alice"})
JavaScript Proxy Cheat Sheet
- ✅
gettrap: intercept property reads and method calls - ✅
settrap: intercept property assignments for validation/reactivity - ✅
applytrap: intercept function calls for memoization/logging - ✅
hastrap: interceptinoperator - ✅
deletePropertytrap: interceptdeleteoperations - ✅ Always use
Reflect.*as fallback in traps for correct default behaviour - ❌ Proxy has overhead — don't use for hot paths with millions of operations/second
- ❌ Cannot proxy primitive values — only objects and functions
Proxy works naturally with the WeakMap memory management patterns — store Proxy handler state in WeakMap to avoid memory leaks when proxied objects are garbage collected. The V8 JIT guide explains why Proxy has performance overhead — the JIT can't optimize through traps as easily as direct property access. External reference: MDN Proxy documentation.
Master advanced JavaScript patterns
→ View Course on Udemy — Hands-on video course covering every concept in this post and more.
Sponsored link. We may earn a commission at no extra cost to you.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
