- 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
undefined
Although we don’t usually write this kind of code, it demonstrates two key things:
Prototype Chain – The
derived
object inherits frombase
.Getter Behavior – The
doubled
getter 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
undefined
Now, 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