Introduction
In Javascript, Proxies enables you to trap specific object operations and customize them. The Proxy acts as an intermediary between the object and the "real world". Thus, you can enhance an object basic operations to implement more complex logic or redefine fundamental operations to fit your needs.
Use cases includes:
- log property accesses, useful for debugging
- validate any interaction with an object (such as form validation)
- helps enforcing consistent formatting for
Proxy takes two parameters:
- target: which is the original object you want to proxy
- handler: an object that defines the operations you will intercept and how you will redefine said operations
Basic example
const target = {
greeting1: "Hello",
greeting2: "Good morning"
}
const handler = {
get(target, prop, receiver) {
return target[prop] + " friends!"
}
}
const proxy = new Proxy(target, handler)
console.log(proxy.greeting1) // Hello friends!
console.log(proxy.greeting2) // Good morning friends!
In this example, we define a Proxy. The target object has two properties. We define a handler that provides an implementation of the get() handler. The get trap intercepts the access to any property on the target object, and within it, we can modify the behavior as needed.
With this setup, it means that every time we are going to access properties in the target object, the handler intercepts it, and runs the code we implemented. In our case, it just takes the property value and adds friends!
.
Reflect
Often times, Proxies are used with the Reflect API. Reflect provides methods with the sames names as the Proxy traps. Like its name indicates, it reflects the semantics for invoking the corresponding object internal methods.
const target = {
greeting1: "Hello",
greeting2: "Good morning"
}
const handler = {
get(target, prop, receiver) {
return Reflect.get(...arguments) + " friends!"
}
}
const proxy = new Proxy(target, handler)
console.log(proxy.greeting1) // Hello friends!
console.log(proxy.greeting2) // Good morning friends!
Reflect is not required to use Proxies but using Reflect allows us to be sure that the behavior matches the native Javascript engine operations. It also ensure compatibility with future updates, prevents unintended side effects and simplifies code. Without it, the developer would have to re-implement behavior like property access, assignment, deletion... which can be error-prone and inconsistent with Javascript's native behavior.
Examples
Let's build some examples to explore what we can do with Proxy.
Logging
In our first example, let's say we wish to log which actions are taken on a object. Whenever we get, set or delete a property, I want to be print out to the console. This could be useful for debugging purposes.
const target = {
name: "Damien",
age: 32,
status: "WRITING"
}
const loggerHandler = {
get(target, prop, receiver) {
if (prop in target) {
console.log(`[LOG] Accessing property ${prop}. Current value is ${target[prop]}`)
return Reflect.get(...arguments)
} else {
console.error(`[LOG] Error accessing non-existent property ${prop}`)
}
},
set(target, key, value) {
console.log(`[LOG] Setting property ${key}. New value: ${value}`)
return Reflect.set(...arguments)
},
deleteProperty(target, prop) {
console.warn(`[LOG] Deleting property: ${prop}`)
return Reflect.deleteProperty(...arguments)
}
}
const proxy = new Proxy(target, loggerHandler)
proxy.name // [LOG] Accessing property name. Current value is Damien
proxy.status // [LOG] Accessing property status. Current value is WRITING
proxy.name = "Bob" // [LOG] Setting property name. New value: Bob
proxy.status = "NAPPING" // [LOG] Setting property status. New value: NAPPING
proxy.job = "Developer" // [LOG] Setting property job. New value: Developer
delete proxy.job // [LOG] Deleting property: job
proxy.job // [LOG] Error accessing non-existent property job
We defined a loggerHandler
that redefines 3 fundamentals operations: get, set and delete. For each action, we log something to the console describing what is happening. The beauty of the Proxy, we don't need to write the console statement everytime. We interact with our object like we always do, and the proxy takes care of the logging behavior. Pretty cool no?
Input validations
In our second example, we will use a proxy to perform input validations for form data.
const validationRules = {
name: value => value.length >= 3 || "Name must be at least 3 characters long",
age: value => Number.isInteger(value) || "Age must be a number",
email: value => value.includes('@') || "Enter a valid email"
}
let formData = {
name: "",
age: null,
email: ""
}
const formHandler = {
set(target, key, value) {
if (typeof value === "string") {
value = value.trim()
}
const validationResult = validationRules[key](value)
if (validationResult !== true) {
console.error(`Validation failed for property ${key}: ${validationResult}`)
return false;
}
return Reflect.set(...arguments)
}
}
const formProxy = new Proxy(formData, formHandler)
formProxy.age = "32 years old" // Validation failed for property age: Age must be a number
formProxy.name = "Da" // Validation failed for property name: Name must be at least 3 characters long
formProxy.email = "damcoss mail.com" // Validation failed for property email: Enter a valid email
formProxy.age = 32 // OK
formProxy.name = "Damien" // OK
formProxy.email = "*Emails are not allowed*" // OK
We define here an object with different methods used to validate whether or not the values are valid. Then, we use the same logic. We have our target object formData
that we want to proxy. In the formHandler
, we redefine the set() method to apply our validation rules on the inputs values.
Conclusion
Proxies, combined with the Reflect API, are flexible and powerful tools to intercept and customize operations on objects. Using them, you can enhance and control behavior dynamically. By using the Reflect API, you also make sure that the behavior is consistent with the Javascript engine.
Proxies are often used in libraries and frameworks to enable advanced behabior like reactive programming, API wrappers and property observation.
Have fun!