Understanding Javascript's Proxies and Reflect API

Damien Cosset - Oct 7 - - Dev Community

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!

Enter fullscreen mode Exit fullscreen mode

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

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

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 = "damcoss@mail.com" // OK
Enter fullscreen mode Exit fullscreen mode

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 ❤️

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .