DEV Community

Cover image for Practical use cases for JavaScript ES6 proxies
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Practical use cases for JavaScript ES6 proxies

Written by Eslam Hefnawy✏️

Metaprogramming is a powerful technique that enables you to write programs that can create other programs. ES6 made it easier to utilize metaprogramming in JavaScript with the help of proxies and many similar features. ES6 proxies facilitate the redefinition of fundamental operations in an object, opening the door for a wide variety of possibilities.

In this guide, we’ll show you how to apply ES6 proxies in practical situations.

Prerequisites and outcomes

This tutorial is primarily aimed at developers who have experience with JavaScript and are at least familiar with the idea of ES6 proxies. If you have a firm understanding of proxies as a design pattern, that knowledge should translate.

After reading this guide, you should be able to:

  • Understand what an ES6 Proxy is, how to implement one, and when to use it
  • Use ES6 Proxies for access control, caching, and data binding

LogRocket Free Trial Banner

Anatomy of an ES6 proxy: Target, handler, and trap

Fundamentally, a proxy is something or someone that becomes something else’s substitute, so that whatever it is, it has to go through the substitute to reach the real deal. An ES6 proxy works the same way.

To effectively implement and use an ES6 proxy, you must understand three key terms:

  1. Target — The real deal that the proxy is substituting, the target is what stands behind the proxy. This can be any object
  2. Handler— An object that contains the logic of all the proxy’s traps
  3. Trap — Similar to traps in operating systems, traps in this context are methods that provide access to the object in a certain way

Putting all this together, below is the simplest implementation in which you can return something different if a given property doesn’t exist in an object using an ES6 proxy.

const target = {
    someProp: 1
}

const handler = {
    get: function(target, key) {
        return key in target ? 
        target[key] : 
        'Doesn't exist!';
    }
}

const proxy = new Proxy(target, handler);
console.log(proxy.someProp) // 1
console.log(proxy.someOtherProp) // Doesn't exist!
Enter fullscreen mode Exit fullscreen mode

An ES6 proxy is a powerful feature that facilitates the virtualization of objects in JavaScript.

Data binding: Syncing multiple objects

Data binding is often difficult to achieve due to its complexity. The application of ES6 proxies to achieve two-way data binding can be seen among model-view-controller libraries in JavaScript, where an object is modified when the DOM undergoes a change.

To put it simply, data binding is a technique that binds multiple data sources together to synchronize them.

Suppose that there is an <input> with the id of username.

<input type="text" id="username" /> 
Enter fullscreen mode Exit fullscreen mode

Let’s say you want to keep the value of this input in sync with a property of an object.

const inputState = {
    id: 'username',
    value: ''
}
Enter fullscreen mode Exit fullscreen mode

It’s quite easy to modify the inputState when the value of the input changes by listening to the change event of the input and then updating inputState‘s value. However, the reverse — updating the input when the inputState is modified — is quite difficult.

An ES6 proxy can help in such a situation.

const input = document.querySelector('#username')
const handler = {
    set: function(target, key, value) {
        if (target.id && key === 'username') {
            target[key] = value;
            document.querySelector(`#${target.id}`)
            .value = value;
            return true
        }
        return false
    }
}

const proxy = new Proxy(inputState, handler)
proxy.value = 'John Doe'
console.log(proxy.value, input.value) 
// 'John Doe' will be printed for both
Enter fullscreen mode Exit fullscreen mode

This way, when the inputState changes, the input will reflect the change that has been made. Combined with listening to the change event, this will produce a simple two-way data binding of the input and inputState.

While this is a valid use case, it’s generally not encouraged. More on that later.

Caching: Enhancing code performance

Caching is an ancient concept that allows very complex and large applications to remain relatively performant. Caching is the process of storing certain pieces of data so they can be served much faster when requested. A cache doesn’t store any data permanently. Cache invalidation is the process of ensuring that the cache is fresh. This is a common struggle for developers. As Phil Karlton said, “There are only two hard things in Computer Science: cache invalidation and naming things.”

ES6 proxies make caching easier. If you want to check whether something exists in an object, for example, it would first check the cache and return the data or do something else to obtain that data if it doesn’t exist.

Let’s say you need to make a lot of API calls to obtain a specific piece of information and do something with it.

const getScoreboad = (player) => {
    fetch('some-api-url')
    .then((scoreboard) => {
        // do something with scoreboard
    })
}
Enter fullscreen mode Exit fullscreen mode

This would mean that whenever the scoreboard of a player is required, a new call has to be made. Instead, you could cache the scoreboard when it is first requested, and subsequent requests tcould be taken from the cache instead.

const cache = { 
    'John': ['55', '99']
}
const handler = { 
    get: function(target, player) {
        if(target[player] {
            return target[player]
        } else {
            fetch('some-api-url')
            .then((scoreboard => {
                target[player] = scoreboard
                return scoreboard
            })
        }
    }
}
const proxy = new Proxy(cache, handler)
// access cache and do something with scoreboard
Enter fullscreen mode Exit fullscreen mode

This way, an API call will only be made if the cache doesn’t contain the player’s scoreboard.

Access control: Controlling what goes in and out of objects

The simplest use case is access control. Most of what the ES6 proxy is known for falls under access control. The scenario we walked through to show how to implement proxies is an example of access control.

Let’s explore a few practical applications of access control using an E6 proxy.

1. Validation

One of the most intuitive use cases for ES6 proxies is validating what comes inside your object to ensure that the data in your object is as accurate as possible. For example, if you want to enforce a maximum number of characters for a product description, you could do so like this:

const productDescs = {}
const handler = {
    set: function(target, key, value) {
        if(value.length > 150) {
            value = value.substring(0, 150)
        }
        target[key] = value
    }
}
const proxy = new Proxy(productDescs, handler)
Enter fullscreen mode Exit fullscreen mode

Now, even if you add a description that’s longer than 150 characters, it’ll be cut short and added.

2. Providing a read-only view of an object

There may come a time when you want to ensure that an object is not modified in any way and can only be used for reading purposes. JavaScript provides Object.freeze() to do this, but the behavior is more customizable when using a proxy.

const importantData = {
    name: 'John Doe',
    age: 42
}

const handler = {
    set: 'Read-Only',
    defineProperty: 'Read-Only',
    deleteProperty: 'Read-Only',
    preventExtensions: 'Read-Only',
    setPrototypeOf: 'Read-Only'
}

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

Now when you try to mutate the object in any way, you’ll only receive a string saying Read Only. Otherwise, you could throw an error to indicate that the object is read-only.

3. Private properties

Javascript doesn’t have private properties per se, except for closures. When the Symbol data type was introduced, it was used to mimic private properties. But it fell by the wayside with the introduction of the Object.getOwnPropertySymbols method. ES6 proxies aren’t a perfect solution, but they do the job in a pinch. A common convention is to identify a private property by prepending an underscore before its name. This convention enables us to use ES6 proxies.

const object = {
    _privateProp: 42
}

const handler = {
    has: function(target, key) {
        return !(key.startsWith('_') && key in target)
    },
    get: function(target, key, receiver) {
        return key in receiver ? target[key] : undefined
    }
}

const proxy = new Proxy(object, handler)
proxy._privateProp // undefined
Enter fullscreen mode Exit fullscreen mode

Adding the ownKeys and deleteProperty will bring this implementation closer to being a truly private property. Then again, you can still view a proxy object’s in the developer console. If your use case aligns with the above implementation, it’s still applicable.

Why and when to use proxies

ES6 proxies are not ideal for performance-intensive tasks. That’s why it’s crucial to perform the necessary testing. A proxy can be used wherever an object is expected, and the complex functionality that proxies provide within a few lines of code makes it an ideal feature for metaprogramming. Proxies are typically used alongside another metaprogramming feature known as Reflect.

Summary

Hopefully, this guide has helped you understand why ES6 proxies are such a great tool, especially for metaprogramming. You should now know:

  • What an ES6 proxy is
  • How and when to implement a proxy
  • How to use ES6 proxies to perform access control, data binding, and caching
  • That ES6 proxies are not ideal for performance-intensive tasks

To learn more, check out the following resources.


LogRocket: Debug JavaScript errors easier by understanding the context

Debugging code is always a tedious task. But the more you understand your errors the easier it is to fix them.

LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to find out exavtly what the user did that led to an error.

LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier.

Try it for free.


The post Practical use cases for JavaScript ES6 proxies appeared first on LogRocket Blog.

Top comments (0)