Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

A 15-Line Proxy That Tells You Exactly What Changed

C
Carlo Fontanos
· 2 min read

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.

C
Written by Carlo Fontanos

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.

Message sent!

Your details are only used to reply to you.

Keep reading