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
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:
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;
},
};
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);
And here we go. You know the API, let’s see some example we can implement.
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);
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'));
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)
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 accesses 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.
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 component 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>
);
}
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 be 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',
},
};
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).
You can find me on Twitter if you want to comment this post or just contact me. Feel free to buy me a coffee if you like the content and encourage me.