Proxy is something that is commonly used in Java, for example in Spring with Aspect-Oriented Programming (@Transactional
, @Security
, ....). But in Javascript, it's not something that it is usually used. Or is it? :)
In this article, you are going to see:
- the principle
- the API
- some examples of what we can do
- performances
- which libraries use it
Principle
The idea is that we are going to create a new object that wraps the original one, and intercepts and redefines fundamental operations such as getting / setting a property, executing a function , ...
More precisely, we are going to proxify a target object (an object
, function
, ...). And define the operations that we want to intercept / reimplement thanks to an object named handler that contains traps (for example get
, set
, ...).
Here is an image to resume this:
API
Now it's time to see how to implement Proxy in JS. The API is simple
const proxy = new Proxy(target, handler);
The handler
is an object containing all the traps
we want to intercept:
const handler = {
get(target, prop, receiver) {
console.log("Getting the property", prop);
return target[prop];
},
set(target, prop, value) {
console.log("Setting the property", prop);
target[prop] = value;
return true;
},
};
Note: You can also use the
Reflect
API to implement the proxy.
Revocable Proxy
It's also possible to create a Proxy that can be revoked thanks to a given function. Once revoked the proxy becomes unusable.
// `revoke` is the function to revoke the proxy
const { proxy, revoke } = Proxy.revocable(target, handler);
Note: It's not able to restore the Proxy once revoked.
And here we go. You know the API, let's see some example we can implement.
Example of trap
Get
Let's start with the get
trap. Its API is:
get(target, property, receiver)
For the example we are going to do a proxy that will throw an error if the user tries to access a property that doesn't exist on the object.
const person = {
firstName: "Bob",
lastName: "TheSponge",
};
const personProxy = new Proxy(person, {
get(target, prop) {
if (!prop in target) {
throw new Error(
`The property ${prop} does not exist in the object`
);
}
return target[prop];
},
});
// Will print: 'Bob'
console.log(personProxy.firstName);
// Will throw an error
console.log(personProxy.unknownProp);
Apply
The apply
trap is the following one:
apply(target, thisArg, argumentsList)
To illustrate it, we are going to implement a withTimerProxy
that will measure the duration of a callback execution.
function aFunction(param) {
console.log("The function has been called with", param);
return "Hello " + param;
}
function withTimerProxy(callback) {
return new Proxy(callback, {
apply(target, thisArg, argumentsList) {
console.time("Duration");
const result = callback.apply(thisArg, argumentsList);
console.timeEnd("Duration");
return result;
},
});
}
const aFunctionWithTimer = withTimerProxy(aFunction);
// Will print:
// The function has been called with World
// Duration: 0.114013671875 ms
// 'Hello World'
console.log(aFunctionWithTimer("World"));
Note: If you want to see more console API you can read my article Unknown console API in JS.
Note: To do such a functionality, we would probably use an High Order Function instead.
Other traps
Here is the exhaustive list of trap you can use:
construct(target, argumentsList, newTarget)
defineProperty(target, property, descriptor)
deleteProperty(target, property)
getOwnPropertyDescriptor(target, prop)
getPrototypeOf(target)
has(target, prop)
isExtensible(target)
ownKeys(target)
preventExtensions(target)
set(target, property, value, receiver)
setPrototypeOf(target, prototype)
Performances?
Recently, I have seen in the react-hook-form
implementation, that Bill decided not to use Proxy
anymore for the tracking of who watch the state of the form because of performances reasons.
Are the performances so bad? Let's try to measure the performance cost when retrieving the value of a simple property.
I will use the benchmark
library. Here is the script I will run:
const Benchmark = require("benchmark");
const suite = new Benchmark.Suite();
const person = {
firstName: "Bob",
lastName: "TheSponge",
};
const personProxy = new Proxy(person, {});
suite
.add("native", () => {
person.firstName;
})
.add("proxy", () => {
personProxy.firstName;
})
.on("cycle", (event) =>
console.log(event.target.toString())
)
.run({ async: true });
The result is the following one:
Of course, the native implementation is faster because it just access the property. The proxy implementation is largely slower than the native one. But I think it's not so bad.
If you search on the internet about, performances of proxy, some people say that it's a tool for development and should not be used in production. Personally, I think it depends on your use case, the amount of data you want to process with Proxy and the performance you want to have. You can test that with a Proof Of Concept (POC).
There are libraries that rely on proxies, which proves that this can be used in production. Let see two of them.
Note: It's good to note that the "selling point" of
react-hook-form
is the performance, so it makes sense not to use Proxy.
Real use case
SolidJS
SolidJS is a declarative library to build UI, that relies on fine grained reactivity. It does not use a virtual DOM (contrary to React).
The way of writing the code is quite similar to React:
- JSX
- Component
- state => signal
- useEffect => createEffect
- useMemo => createMemo
- ...
But there is no hook rules, you should not destructure your props, every components executes ones then it will execute side effect when a used reactive primitive has changed.
It uses Proxy for store which is the equivalent of React reducers.
If you don't know SolidJS, go check it has a promising future.
For example here is a simple Counter
component:
import { createSignal } from 'solid-js';
function Counter() {
const [count, setCount] = createSignal(0);
const increment = () => setCount(count() + 1);
return (
<button type="button" onClick={increment}>
{count()}
</button>
);
}
Note: This is not an exhaustive list of similarities / differences and how to use the library. When I feel more confident, I will try to write an article about it because I think it has a bright future ahead of it.
ImmerJS
ImmerJS allows us to create immutable data structures, but giving us the possibility to change its data in a mutable way.
For example, you will able to do:
import product from "immer";
const person = {
firstName: "Bob",
lastName: "TheSponge",
};
const newPerson = produce(person, (p) => {
p.firstName = "Patrick";
p.lastName = "Star";
});
// Will return false
console.log(newPerson === person);
It's a convenient way to simplify changes without mutates an object, and without to make a lot of copy.
const person = {
firstName: "Bob",
lastName: "TheSponge",
address: {
type: "pineapple",
city: "Bikini bottom",
},
};
// It prevents us to make things like
const newPerson = {
...person,
address: {
...person.address,
// Note the uppercase
type: "Pineapple",
},
};
Note:
Redux Toolkit
uses it under the hood to make reducers easier to implement.
Conclusion
Proxy enables a lot of features, thanks to them you can extract some logic that will be reusable. Some libraries use them to optimize your code execution. For example react-tracked
and proxy-memoize
that use, proxy-compare
under the hood, will reduce your re-render. These libraries are developed by Dai Shi who also made the use-context-selector
library that I demystified in the article use-context-selector demystified.
But I would recommend you to use them uniquely when it's necessary.
There is a TC39 proposal to add natively Decorators to javascript, looks promising, which will enable some other possibilities. For example to log / measure performance of function, ... in an easy way: with a @Logger
or @Performance
(with the right implementation behind these annotations).
Do not hesitate to comment and if you want to see more, you can follow me on Twitter or go to my Website.
Top comments (4)
Good article. They're not AS performant as "native" logic and that is understandable. Same thing happens to many methods (like iteration) and people still use them because 35million ops per second is ... still a lot. In the end it depends in the internal logic of your methods. If the impact is ineligible you usually have at your hands a really good API to be used (which would require a lot of work to remake btw).
Btw in the API section the 'set' trap should return true ("[[Set]] must return true if the value was written successfully") else it'll throw an error when adding new properties.
Oh thank you. I fix this right away :*
Thanks for the article.
Vue also uses proxy for it's reactivity system.
Thanks for the read and the comment.
Oh I didn't know that. Thank you for the information :)