Proxy: Interceptando datos

Reactividad basada en proxies con Javascript nativo


En el post anterior, explicamos la importancia de la reactividad en Javascript, y lo deseable que es, ya que en Javascript por defecto es inexistente. Sin embargo, existen algunos mecanismos nativos para crear sistemas reactivos o estructuras que puedan ser reactivas.

Una de ellas es la utilización de un tipo de dato específico llamado Proxy.

¿Qué es Proxy?

Los Proxy de Javascript son un tipo de dato que nos permiten «envolver» e interceptar operaciones sobre objetos de Javascript, y ejecutar una lógica asociada cuando ocurren. Con ello podemos tener un control de total de lo que ocurre cuando se interactua con un objeto, y además, dotar de capacidades reactivas personalizadas.

Para utilizar los Proxy debemos tener en cuenta los datos que debemos pasarle:

const proxy = new Proxy(target, handler);
  • 1️⃣ target: Este es el objeto que será envuelto y al que se le interceptarán las operaciones.
  • 2️⃣ handler: Este será un objeto que definirá las operaciones que quieres interceptar como métodos.

Por ejemplo, los métodos más habituales suelen ser get() y set(), que son muy similares a los de una clase. Veamos un ejemplo sencillo que intercepta operaciones en la propiedad name de un objeto:

const state = {
  name: "Unknown username"
}

const proxy = new Proxy(state, {
  get(data, prop) {
    console.log(`Accediendo a ${prop}`);
    return data[prop];
  },
  set(data, prop, value) {
    console.log(`Modificando ${prop} con ${value}`);
    data[prop] = value;
    return true;
  }
});

console.log(proxy.name);  // "Accediendo a name" ("Unknown username")
proxy.name = "ManzDev";   // "Modificando name con ManzDev"
console.log(proxy.name);  // "Accediendo a name" ("ManzDev")

Observa que el método get() está interceptando cada vez que se accede a la propiedad name. Por otro lado, el método set() está interceptando cada vez que se modifica la propiedad name. Así pues, podemos crear la lógica necesaria para que se ejecute cuando ocurra cualquiera de las operaciones mencionadas.

Lista de interceptores

Hemos utilizado los interceptores get() y set() en el ejemplo anterior, que se utilizan para interceptar cuando se modifican las propiedades de un objeto o cuando se acceden o consultan sus propiedades. Sin embargo, existen muchas otras:

Interceptor¿Qué se intercepta?
defineProperty()Definición y modificación de propiedades de un objeto.
has()Comprobación de existencia de una propiedad. Equivalente a prop in obj.
get()Acceso a las propiedades de un objeto. Equivalente a obj[prop].
set()Cambio de una propiedad de un objeto. Equivalente a obj[prop] = value.
deleteProperty()Eliminación de propiedades de un objeto. Equivalente a delete obj[prop].
ownKeys()Obtención de las propiedades de un objeto. Equivale a Object.keys() o similares.
getPrototypeOf()Acceso al prototipo de un objeto.
setPrototypeOf()Modificación del prototipo de un objeto.
isExtensible()Verificación de extensibilidad de un objeto (recibir nuevas propiedades).
preventExtensions()Detención de la capacidad de un objeto de ser extensible.
getOwnPropertyDescriptor()Consulta de propiedades de un objeto (configurable, enumerable...)

Aunque lo habitual es utilizar interceptores como get(), set() y has(), muchos otros interceptores pueden ser realmente útiles en ciertos casos específicos.

¿Qué es Reflect?

Junto a Proxy de Javascript, es muy habitual que utilicemos Reflect. Reflect es un objeto con varios métodos estáticos (los mismos de la tabla anterior). Aunque no es obligatorio su uso, estos métodos son equivalentes a operaciones nativas de Javascript, pero más seguros, consistentes y fáciles de mantener.

  /* ... */
  get(data, prop) {
    /* ... */
    const value = data[prop];                 // Sin Reflect
    const value = Reflect.get(data, prop);    // Con Reflect
  }
  /* ... */
  • 1️⃣ En el caso de que la propiedad prop no exista, es más seguro usar Reflect.
  • 2️⃣ Evita errores o la necesidad de comprobaciones.
  • 3️⃣ Es más fácil de reutilizar, ya que el proxy puede ser más genérico.

Reactividad con Proxy y Reflect

Ahora que ya conocemos Proxy y Reflect, veamos nuestro ejemplo del contador donde implementemos ambos y obtengamos una estructura realmente reactiva:

<div class="counter">-</div>
<button onclick="counter.decrement()">-</button>
<button onclick="counter.increment()">+</button>

<script>
const counterElement = document.querySelector(".counter");

const data = {
  value: 0,
  increment() { this.value++; },
  decrement() { this.value--; }
};

const counter = new Proxy(data, {
  set(data, prop, value) {
    const updatedValue = Reflect.set(data, prop, value);
    counterElement.textContent = data[prop];
    return updatedValue;
  }
});

counterElement.textContent = counter.value;
</script>

En primer lugar, observa que tenemos la estructura de datos data, que tiene el valor correspondiente y los métodos increment y decrement para alterar el valor del contador.

Mediante nuestro proxy counter vamos a envolver la estructura de datos data y definir lo que va a ocurrir cada vez que se cambie el valor del contador:

  • 1️⃣ Creamos una variable updatedValue que refleja el cambio en la estructura de datos.
  • 2️⃣ Actualizamos la interfaz de usuario con los nuevos datos.
  • 3️⃣ Devolvemos un true si ha ido todo bien, o false en caso contrario.

Perfecto. Ahora cada vez que modifiquemos los valores, como en los botones cuando se llama a counter.increment() o counter.decrement(), se lanzarán esos métodos y se actualizará la UI.

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev