A 15-Line Proxy That Tells You Exactly What Changed
The "you have unsaved changes" warning is deceptively annoying to build. Compare snapshots? Deep-equality on every keystroke? Set a flag in forty onChange handlers? Proxy solves it at the root: intercept the assignment itself.
function trackChanges(target) {
const dirty = new Set();
const proxy = new Proxy(target, {
set(obj, prop, value) {
if (obj[prop] !== value) dirty.add(prop);
obj[prop] = value;
return true; // set traps must return true
},
deleteProperty(obj, prop) {
dirty.add(prop);
delete obj[prop];
return true;
},
});
return { proxy, dirty, isDirty: () => dirty.size > 0 };
}
const { proxy: settings, dirty, isDirty } = trackChanges({ theme: 'dark', perPage: 20 });
settings.perPage = 50;
isDirty(); // true
[...dirty]; // ['perPage'] - not just THAT it changed, but WHAT
Every write anywhere in the codebase goes through the trap - no discipline required from callers, no events to remember to fire. Wire isDirty() to beforeunload and the unsaved-changes warning is done:
addEventListener('beforeunload', (e) => {
if (isDirty()) e.preventDefault(); // browser shows its native prompt
});
Two upgrades worth knowing
Nested objects: the trap above only sees top-level writes; settings.colors.accent = 'red' goes through the get trap, not set. The standard fix is lazy wrapping - in the get trap, if the value is an object, return trackChanges of it (cache the wrapper in a WeakMap so identity stays stable). That recursive version is how Vue 3's reactivity actually works under the hood.
Undo history: record {prop, oldValue} pairs in the set trap instead of a Set, and you have a working undo stack in ten more lines. For settings panels and small editors, it's shockingly little code.
Honest limits
- Proxied property access costs roughly an order of magnitude more than plain access. Irrelevant for form/settings objects touched by humans; wrong for a particle system's hot loop.
- Identity splits: proxy !== target, so mixing both references around the codebase confuses === checks and WeakMap keys. Hand out only the proxy.
- Some built-ins (Map, Set, private fields) need extra trap work - wrap plain data objects, not class instances, and life stays simple.
Proxy has a reputation as framework-author machinery, and it is - but a fifteen-line tracker for "did the user edit this config" is exactly the size of problem where using the platform beats importing a state library.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.