Proxy, Reflect & Metaprogramming
Proxy and Reflect are the metaprogramming layer of JavaScript. Every reactive framework â Vue 3's reactivity, MobX observables, Immer's drafts â is built on Proxy. Understanding these primitives means understanding how modern state management actually works under the hood.
Proxy Fundamentals
A Proxy wraps an object and intercepts operations through traps:
const handler = {
get(target, property, receiver) {
console.log(`Reading ${String(property)}`);
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
console.log(`Writing ${String(property)} = ${value}`);
return Reflect.set(target, property, value, receiver);
},
};
const user = new Proxy({ name: 'Alex', age: 30 }, handler);
user.name; // logs: Reading name â "Alex"
user.age = 31; // logs: Writing age = 31Available Traps
| Trap | Intercepts | Example Operation |
|---|---|---|
get | Property read | obj.prop, obj[key] |
set | Property write | obj.prop = value |
has | in operator | 'prop' in obj |
deleteProperty | delete | delete obj.prop |
ownKeys | Key enumeration | Object.keys(obj) |
apply | Function call | fn() |
construct | new operator | new Fn() |
getPrototypeOf | Prototype access | Object.getPrototypeOf(obj) |
defineProperty | Property definition | Object.defineProperty(obj, ...) |
Reflect: The Default Behavior API
Reflect provides the default behavior for every proxy trap. Always use Reflect inside traps instead of direct operations:
const handler = {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
return Reflect.set(target, prop, value, receiver);
},
};Why Reflect instead of direct access? Reflect methods:
- Return booleans for success/failure (set, defineProperty, deleteProperty)
- Properly handle prototype chains via
receiver - Mirror every proxy trap 1:1
- Are the correct forwarding mechanism for invariant-preserving proxies
Reactive State (Vue 3 Pattern)
const targetMap = new WeakMap();
let activeEffect = null;
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key);
const result = Reflect.get(target, key, receiver);
return typeof result === 'object' && result !== null
? reactive(result)
: result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) trigger(target, key);
return result;
},
});
}
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, (depsMap = new Map()));
let deps = depsMap.get(key);
if (!deps) depsMap.set(key, (deps = new Set()));
deps.add(activeEffect);
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
if (deps) deps.forEach(effect => effect());
}
function watchEffect(fn) {
activeEffect = fn;
fn();
activeEffect = null;
}Usage mirrors Vue 3's composition API:
const state = reactive({ count: 0, name: 'Alex' });
watchEffect(() => {
console.log(`Count is: ${state.count}`);
});
state.count++; // logs: Count is: 1
state.count++; // logs: Count is: 2
state.name = 'Sam'; // no log â not tracked by the effectValidation Proxy
function validated(schema) {
return new Proxy({}, {
set(target, prop, value) {
const validator = schema[prop];
if (validator && !validator(value)) {
throw new TypeError(`Invalid value for ${String(prop)}: ${value}`);
}
return Reflect.set(target, prop, value);
},
get(target, prop, receiver) {
if (!(prop in target)) {
throw new ReferenceError(`Property ${String(prop)} not defined`);
}
return Reflect.get(target, prop, receiver);
},
});
}
const user = validated({
age: v => typeof v === 'number' && v >= 0 && v <= 150,
email: v => typeof v === 'string' && v.includes('@'),
name: v => typeof v === 'string' && v.length > 0,
});
user.name = 'Alex'; // OK
user.age = 30; // OK
user.age = -1; // TypeError: Invalid value for age: -1
user.email = 'invalid'; // TypeError: Invalid value for email: invalidImmutable Proxy (Immer Pattern)
function createDraft(base) {
const modified = new Set();
const copy = { ...base };
return new Proxy(copy, {
get(target, prop) {
return Reflect.get(target, prop);
},
set(target, prop, value) {
modified.add(prop);
return Reflect.set(target, prop, value);
},
deleteProperty(target, prop) {
modified.add(prop);
return Reflect.deleteProperty(target, prop);
},
});
}
function produce(base, recipe) {
const draft = createDraft(base);
recipe(draft);
return Object.freeze({ ...draft });
}
const state = { users: ['Alex'], count: 1 };
const next = produce(state, draft => {
draft.count = 2;
draft.users = [...draft.users, 'Sam'];
});Revocable Proxies
For access control â create a proxy that can be permanently disabled:
const { proxy, revoke } = Proxy.revocable(sensitiveData, handler);
proxy.secret; // works
revoke();
proxy.secret; // TypeError: Cannot perform 'get' on a proxy that has been revokedProxy Limitations
- Performance â Proxy access is slower than direct property access (2-5x in microbenchmarks). Avoid on hot paths
- Identity â
proxy !== target. This can break===checks, Map/Set lookups, and WeakMap keys - Private fields â Proxy cannot intercept private class fields (
#field), because the private slot exists on the target, not the proxy - Built-in exotic objects â Date, RegExp, Map, Set have internal slots that Proxy can't intercept. Wrapping them requires forwarding methods explicitly
const dateProxy = new Proxy(new Date(), {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
return typeof value === 'function' ? value.bind(target) : value;
},
});
dateProxy.getTime(); // works because we bind to targetInterview Signal
Senior candidates demonstrate:
- Trap mechanics â How traps map to language operations, the role of
receiverin prototype chains - Reflect discipline â Always forwarding through Reflect, not direct property access
- Reactive systems â Can sketch Vue 3 / MobX reactivity using Proxy + dependency tracking
- Limitations awareness â Performance cost, identity issues, private fields, exotic objects
- Real-world applications â Validation, immutability (Immer), revocable access, API mocking