DEV Community

Cover image for Proxy in JS: what the hell?
Romain Trotard
Romain Trotard

Posted on • Edited on • Originally published at romaintrotard.com

Proxy in JS: what the hell?

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:

Javascript proxy


API

Now it's time to see how to implement Proxy in JS. The API is simple

const proxy = new Proxy(target, handler);
Enter fullscreen mode Exit fullscreen mode

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;
  },
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Apply

The apply trap is the following one:

apply(target, thisArg, argumentsList)
Enter fullscreen mode Exit fullscreen mode

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"));
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

The result is the following one:

Performance result

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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",
  },
};
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
nicoh profile image
Nico Hevia

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.

set(target, prop, value) {
    console.log("Setting the property", prop);
    target[prop] = value;
    return true
},
Enter fullscreen mode Exit fullscreen mode
Collapse
 
romaintrotard profile image
Romain Trotard

Oh thank you. I fix this right away :*

Collapse
 
hendrikjan profile image
H.J. van Meerveld

Thanks for the article.
Vue also uses proxy for it's reactivity system.

Collapse
 
romaintrotard profile image
Romain Trotard

Thanks for the read and the comment.
Oh I didn't know that. Thank you for the information :)