Recursive Proxies to Support Nested Objects

Two Approaches: Eager Wrapping and Lazy Wrapping

Hi,

This post is part of the series on learning about Proxies used in Valtio and some of my other libraries. Today, we will learn how to support nested objects.

The Problem

First, let’s look at the issue:

const obj = { nested: { count: 0 } };

const p = new Proxy(obj, {
  get(target, prop, receiver) {
    console.log('getting', prop);
    return Reflect.get(target, prop, receiver);
  },
});

p.nested.count; // Logs "getting nested", but we can't track access to count.

Typically, we would like to track nested objects deeply, not just the top-level object. There are two approaches to achieve this.

Approach 1: Eager Wrapping

The eager approach wraps all nested objects with Proxies at initialization:

const p = new Proxy({
  nested: new Proxy({ count: 0 }, { get(...) { ... } }),
}, { get(...) { ... } });

This is a naive implementation, but you get the idea. We can create a function that recursively wraps all nested objects with Proxies.

Pros and Cons of Eager Wrapping

✅ Simple logic – The implementation is straightforward.
✅ Predictable computation – The structure is fully wrapped from the beginning.
❌ Potential waste – If some leaf objects are never accessed, wrapping them upfront is unnecessary.

This is where the second approach comes in.

Approach 2: Lazy Wrapping

The lazy approach wraps nested objects only when they are accessed:

const p = new Proxy({ nested: { count: 0 } }, {
  get(target, prop) {
    return new Proxy(target[prop], { get(...) { ... } });
  },
});

This is a buggy implementation, but it illustrates the idea. The issue here is that a new Proxy is created every time the property is accessed. This is not only wasteful but can also lead to unexpected behavior in certain cases.

To fix this, we need to cache and reuse the created Proxies instead of generating a new one every time. While this makes the implementation more complex, it ensures that Proxies are created only when necessary.

Pros and Cons of Lazy Wrapping

✅ Efficient resource usage – Proxies are created only when needed.
❌ Less predictable behavior – The first access to an object behaves differently from subsequent accesses, which could cause subtle issues in edge cases.

How Valtio Uses These Approaches

In practice, Valtio takes a mixed approach:

  • The eager approach is used for the proxy function.

  • The lazy approach is used for the useSnapshot function.

This is a subtle design decision based on expected usage patterns.

Happy coding.

Reply

or to participate.