Proxy use cases

TL;DR Proxy allows you to intercept all object prototype methods.

new Proxy(object, {
    get: (obj, key) => { return 42 }
}) // the answer to everything is 42

What is Proxy?

Proxy is a javascript class that allows you to intercept any type of call made to an object and respond in with your own methods instead of having the object respond with its native prototype methods.

The simplest example to understand is to use a proxy to provide a default value:

const object = {a: 1, b: 2}
const proxy = new Proxy(object, {
    get: (obj, key) => key in obj ? obj[key] : 42
})
console.log(proxy.a) // 1
console.log(proxy.c) // 42

There are many more methods than just get that Proxy can intercept, take a look at the MDN docs to learn more.

Another classic is to add proxied keys that return values not stored in the object but computed from its values:

const handler = {
    get: (obj, key) => {
        if(key !== 'fullName')
            return obj[key]
        if(obj.firstName && obj.lastName)
            return `${obj.firstName} ${obj.lastName}`
        else 
            throw new ReferenceError(`Cannot compute ${key} from existing keys`)
    }
}

Though Object.defineProperty would be a better tool for this job because it targets a specific property instead of the prototype method itself:

Object.defineProperty(obj, 'fullName', {
    get() { return `${obj.firstName} ${obj.lastName}` }
})

However, proxies are much more powerful than Object.defineProperty because they allow you to define how all get calls are handled and not just how it is handled for one specific key.


Real world example #1: fallback dictionnary

For example, we can use it to define a fallback dictionnary:

const defaultMessages = {
    warn: 'pretty sure you should not be doing this',
    error: 'you definitely made a mistake there pal'
}
const politeMessages = {
    warn: 'we recommend you do things differently'
}
const proxiedMessages = new Proxy(politeMessages, {
    get: (obj, key) => {
        if(key in obj)
            return obj[key]
        else
            return defaultMessages[key]
    }
})
console.log(proxiedMessages.warn)
// we recommend you do things differently
console.log(proxiedMessages.error)
// you definitely made a mistake there pal

Real world example #2: private keys

From there, it’s just up to your imagination! How about “private” keys:

const handler = {
    get: (obj, key) => {
        if(key.startsWith('_'))
            throw new ReferenceError(`Key ${key} is private`)
        return obj[key]
    },
    set: (obj, key, val) => {
        if(key.startsWith('_'))
            throw new ReferenceError(`Key ${key} is private`)
        obj[key] = val
    }
}
const proxy = new Proxy(obj, handler)

And by the way, Proxy allows you to intercept all of the object’s methods. And if you wanted to push your “private” keys further, you could add the ownKeys, has and deleteProperty traps to your handler:

handler.ownKeys = (obj) => {
    return Object.keys(obj).filter(key => !key.startsWith('_'))
}
handler.has = (obj, key) => {
    if(key.startsWith('_'))
        return false
    return key in obj
}
handler.deleteProperty = (obj, key) => {
    if(key.startsWith('_'))
        throw new ReferenceError(`Key ${key} is private`)
    delete obj[key]
}

And now, even Object.keys or Object.getOwnPropertyNames could not reach your object’s private keys!


Real world example #3: temporary state

Imagine you are programming a game in which a player can be affected temporarily with different conditions. We can make a proxied object that will automatically return an up to date state when accessed!

const playerCondition = new Proxy({}, {
    set: (obj, key, value) => {
        obj['_'+key] = Date.now() + 5000
        obj[key] = value
    },
    get: (obj, key) => {
        if(obj['_'+key] > Date.now())
            return obj[key]
        return undefined
    }
})

playerCondition.paralysed = true
console.log(playerCondition.paralysed) // true
// wait for 5 seconds
console.log(playerCondition.paralysed) // undefined

Real world example #4: memoization of function calls

A Proxy can also be used to intercept calls to a function.

// function returning random integers
const fn = (arg) => Math.floor(Math.random() * Math.floor(100))

const handler = {
    apply: (fn, store, [arg]) => {
        if (store[arg])
            return store[arg]
        const result = fn(arg)
        store[arg] = result
        return result
    }
}

const proxy = new Proxy(fn, handler).bind({})

console.log(proxy('/yo'))   // 86
console.log(proxy('/yo'))   // 86
console.log(proxy('/lala')) // 21
console.log(proxy('/lala')) // 21

You might notice the bind({}) call on the proxy declaration: this is to provide a scope for the function to store its data. In the handler’s apply trap, the variable store refers to this very scope.


Real world example #5: refreshing stored network requests

Combining the two previous examples, expiration date and memoization, let’s simulate fetch calls with a refresh duration. This is a very typical case for authentication tokens that remain valid for some duration but expire after a while and need to be refreshed.

const handler = {
    apply: async (fn, store, [url]) => {
        if (store['_'+url] > Date.now())
            return store[url]
        const result = await fn(url)
        store[url] = result
        store['_'+url] = Date.now() + 60000 // a minute from now
        return result
    }
}
const proxy = new Proxy(fetch, handler).bind({})
const url = '/api/endpoint'

proxy(url).then(console.log) // fresh data, from the `fetch` call
// after a few seconds
proxy(url).then(console.log) // stale data, from the `store` object 
// after a minute
proxy(url).then(console.log) // fresh data, from the `fetch` call

In this example, we’re writing a proxy around the actual native fetch function, which I find pretty cool!


Real world example #6: event based state

If you want to monitor the changes made to an object, you can use a Proxy to dispatch an event every time such a change occurs. This is useful in an event based architecture if you want to update your DOM based on a JSON state object for example.

const state = new Proxy(new EventTarget(), {
    get: (obj, key) => {
        const result = Reflect.get(obj, key)
        if(typeof result === 'function')
            return result.bind(obj)
        return result
    },
    set: (obj, key, value) => {
        const result = Reflect.set(obj, key, value)
        if(result)
            obj.dispatchEvent(new CustomEvent('change', {
                detail: {key, value}
            }))
        return result
    },
    deleteProperty: (obj, key) => { 
        const result = Reflect.deleteProperty(obj, key) 
        if(result)
            obj.dispatchEvent(new CustomEvent('change', {
                detail: {key, value: undefined}
            }))
        return result
    } 
})
state.addEventListener('change', e => console.log(e.detail))
state.foo = 'bar' // {key: "foo", value: "bar"}
delete state.foo  // {key: "foo", value: undefined}

PS: if the use of Reflect is confusing to you, I wrote an article about how it relates to Proxy!