- Daishi Kato's Read the Code
- Posts
- Why We Use Reflect for Proxy Handlers
Why We Use Reflect for Proxy Handlers
Valtio Uses It to Cover Rare Use Cases
Hi,
In my previous post, I described the default behavior of a Proxy get handler as "more or less" correct because the implementation was naive. Let’s explore an edge case that breaks a naive implementation.
We’ll focus on the get handler in this post, but the same concept applies to all handlers.
Understanding the Edge Case
Before diving into Proxies, let’s first look at a JavaScript behavior that is important to understand. Consider the following code:
> const base = { count: 1, get doubled() { return this.count * 2 } };
undefined
> const derived = Object.create(base);
undefined
> derived.count = 2;
2
> console.log(derived.doubled);
4
undefinedAlthough we don’t usually write this kind of code, it demonstrates two key things:
Prototype Chain – The
derivedobject inherits frombase.Getter Behavior – The
doubledgetter usesthis, which refers to the calling object (derived).
Now, let’s see what happens when we introduce a naive Proxy.
> const base = { count: 1, get doubled() { return this.count * 2 } };
undefined
> const naiveProxy = new Proxy(base, { get(target, prop) { return target[prop]; } });
undefined
> const derived = Object.create(naiveProxy);
undefined
> derived.count = 2;
2
> console.log(derived.doubled);
2
undefined We expected 4, but got 2. This happens because our get handler directly returns base.doubled, rather than respecting the prototype chain. The this inside the doubled getter still refers to base, not derived.
The Solution: Using Reflect.get
How can we fix this? The correct way is to use Reflect.get.
> const base = { count: 1, get doubled() { return this.count * 2 } };
undefined
> const reflectProxy = new Proxy(base, { get(target, prop, receiver) { return Reflect.get(target, prop, receiver); } });
undefined
> const derived = Object.create(reflectProxy);
undefined
> derived.count = 2;
2
> console.log(derived.doubled);
4
undefinedNow, it works as expected, just like our original example without using Proxies.
Why Reflect.get Works
In our fixed implementation, the get handler receives an additional argument: receiver. This represents the value of this (derived in our case). When Reflect.get(target, prop, receiver) is used, it ensures that the getter is called with the correct this value, preserving expected behavior.
Best Practice: Always Use Reflect
This is a tricky issue when working with Proxies, but the solution is simple: always use Reflect in Proxy handlers. This is exactly how it is done in Valtio's codebase.
Happy coding.
Reply