Skip to Content
Course content

199: Practice Exercise: Building an Observable Object with Proxies

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When people first start playing with Proxy, they often fall into the trap of thinking that a Proxy is like a "magic shield" that covers everything inside an object. I see this all the time: a developer wraps a configuration object in a Proxy to detect changes, and then they're baffled when their nested properties—like settings.theme.color—change without triggering a single trap.

The Myth of Automatic Deep Tracking

The misconception is that the set trap is recursive. It isn't. A Proxy only intercepts operations on the immediate target it is wrapping. If you access a nested object, the Proxy simply returns that nested object as a regular, non-proxied object. Once you're "inside" that nested object, you've stepped outside the Proxy's jurisdiction.

const state = {
  user: { name: 'Alice', preferences: { theme: 'dark' } }
};

const observedState = new Proxy(state, {
  set(target, prop, value) {
    console.log(`Property ${prop} changed to ${value}`);
    target[prop] = value;
    return true;
  }
});

observedState.user = { name: 'Bob' }; // Logs: Property user changed...
observedState.user.name = 'Charlie'; // SILENCE. No log.

In the example above, the second assignment doesn't trigger the trap because observedState.user returned the original nested object. You aren't calling set on the Proxy anymore; you're calling set on a plain JavaScript object.

Recursive Proxies: Making the Observation Sticky

To build a truly observable object, we have to be intentional. The trick is to intercept the get trap. When a user requests a property that happens to be another object, we don't just hand it over—we wrap that nested object in a Proxy too, using the same handler. I like to think of this as "infecting" the object tree with observability as you traverse it.

const handler = {
  get(target, prop, receiver) {
    const value = Reflect.get(target, prop, receiver);
    
    // If the value is an object, wrap it in a proxy before returning it
    if (typeof value === 'object' && value !== null) {
      return new Proxy(value, handler);
    }
    
    return value;
  },
  set(target, prop, value) {
    console.log(`Change detected: ${prop} is now ${value}`);
    return Reflect.set(target, prop, value);
  }
};

const appState = new Proxy({ 
  settings: { theme: 'light' } 
}, handler);

appState.settings.theme = 'dark'; // Logs: Change detected: theme is now dark

By doing this, we've created a dynamic chain. It doesn't matter how deep your data structure goes; the moment you touch a nested object, it gets proxied on the fly. One small tip: I used Reflect here instead of direct assignment. While target[prop] = value works most of the time, Reflect is the professional standard when working with Proxies because it handles edge cases (like inherited properties) much more predictably.




📋 Practical Task

Exercise: Building a Reactive Store with Change Callbacks

Instead of just logging to the console, let's build a primitive "Store" that allows us to register a callback function that runs whenever any property—at any depth—is modified. This is essentially how the core of frameworks like Vue.js handles reactivity.

Requirements:

  • Create a function createObservableStore(initialState, onChangeCallback).
  • The function should return a Proxy that implements the recursive "sticky" observation pattern we discussed.
  • Whenever a property is changed (at the top level or within a nested object), the onChangeCallback must be executed.
  • The callback should receive the name of the property that changed and the new value.

Test your implementation with this snippet:

const myStore = createObservableStore(
  { profile: { name: 'Dev', stats: { level: 1 } } }, 
  (prop, val) => console.log(`Store Update -> ${prop}: ${val}`)
);

myStore.profile.name = 'Senior Dev'; // Should trigger callback
myStore.profile.stats.level = 2;    // Should trigger callback
Rating
0 0

There are no comments for now.

to be the first to leave a comment.