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 usarReflect
. - 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, ofalse
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.