Intro to Proxy

Modern JavaScript is amazing. There are so many features you might not have had a chance to use, and some that are probably being utilized by some of your favorite frameworks and tooling. One such feature is the Proxy. A Proxy allows you to wrap and intercept methods and properties of other objects. You might want to validate data or simply notify parts of your application when some change to the Proxy object has occurred. Proxy gets its name because it acts as the Proxy or middle-man for your target object and your handlers. The target object isn’t modified, and changes to that target won’t invoke any Proxy intercept handlers.

Proxy Object Basics

A Proxy is going to need a handler object. This handler object will contain any methods you want to intercept on the target object. This could be as simple as a get and set method. These are referred to as Proxy traps, because you are going to trap data and do something with it. Maybe we want to make sure that only strings can be provided as values to properties of an empty object.

const handler = {
	get(target, prop) {
		// always return in UPPERCASE
		return target[prop]?.toUpperCase();
	},
	set(target, prop, value) {
		if (typeof value !== "string") {
			throw new Error("Value can only be string!");
		}
		target[prop] = value;
		return true;
	}
}

const user = new Proxy({}, handler);
user.name = "tony"
user.age = 32; // throws an error, but change to user.age = "32" and it will work.

// Set property correctly
user.name = "tony"
user.age = "32";

console.log(user.name); // TONY

You may be wondering why we are using optional chaining in the getter (target[prop]?.toUpperCase()), and that is because sometimes the getter might be in reference to a function, and that function won’t have a toUpperCase method. For example, if I want to use JSON.stringify() on an object, the toJSON method of the object will be used, and we need to be able to account for that when using Proxies.

console.log(JSON.stringify(user)); // {"name":"TONY","age":"32"}

This pattern of using optional chaining works in this case when a function is accessed, but it isn’t ideal. This brings us to another useful tool when working with JavaScript Proxies and that is Reflect.

Reflect on your decisions

The Reflect object allows you to use the default behavior of the object in a Proxy. This means that if you come across a function in your handlers getter that you are not concerned with, you can use Reflect to use the default behavior of that object.

// A slightly modified version of our earlier example
const handler = {
	get(target, prop) {
		if (typeof target[prop] === "function") {
			// Any functions can easily forward their
			// default implementation using Reflect
			return Reflect.get(target, prop);
		}
		// always return in UPPERCASE
		return target[prop]?.toUpperCase();
	},
	set(target, prop, value) {
		if (typeof value !== "string") {
			throw new Error("Value can only be string!");
		}
		target[prop] = value;
		return true;
	}
}

const user = new Proxy({}, handler);
user.name = "tony"
user.age = "32";
console.log(user.hasOwnProperty("address")); // false

In this snippet, we are trying to access the native hasOwnProperty method of the object. We are not interested in intercepting this method, so we can use Reflect to use the default behavior. There are a number of methods available in the Reflect object that can be used with Proxy objects, or any other objects. We won’t dive into details on these. For now, we’ve laid the foundation to really get some powerful use from Proxy.

Application State

Proxy is used in some popular state management libraries for this very purpose. Let’s take a look at how you can use Proxy in your own applications. Assume you want to know each time a state object in your application has been updated. You can use a callback in the setter of your handler for the Proxy to intercept these changes.

function createStore(target, listener) {
  const handler = {
    set(target, prop, value, receiver) {
				target[prop] = value;
				listener(receiver);
				return true;
		},
    get(target, prop) {
    	return target[prop];
  	},
  };
  return new Proxy(target, handler);
}

const initialState = {
	authenticated: false,
	userName: "Sam",
	favorites: []
};

function appListener(state) {
	if (state.authenticated) {
		 // update UI
	}
	if (state.favorites.length) {
		// update UI
	}
}

const store = createStore(initialState, appListener);

// Some other part of the application makes
// these updates
store.authenticated = true;
store.favorites = ["Pizza", "Tacos"];

What we can do here is write a method called createStore that will create an observable object from a Proxy that we can use. Other parts of our application can update this proxied object and our listener callback will let us know that changes have happened. It’s up to us to determine how to handle those changes.

We could take this a step further and let the callback know what property was changed, so we can only react to those specific changes. We can update the setter to pass back the property name.

listener(observable, prop);

Now that the property name is passed to the callback, we can better filter the updates we do in our application listener.

function appListener(state, prop) {
	if (prop === "authenticated") {
		updateAuthUI(state);
	}
	else if (prop === "favorites") {
		updateFavoritesList(state);
	}
}

This allows you to design your application state management to be flexible to your particular needs.

Proxy Arrays

We’ve seen how you can Proxy an object and account for functions using Reflect. However, what if we wanted to intercept specific functions of a JavaScript object and react to the changes they make? That could make an interesting use case for using an Array with Proxy.

There are a handful of methods that will mutate an array.

  • push
  • pop
  • shift
  • unshift
  • splice

We could have a handler for our Proxy that would only concern itself with these methods. We can intercept these functions and replace them with our own functions that add the ability to use a callback. Our application can use that callback to track changes in the array.

function createObservableArray({ target, listener }) {
	const handler = {
		get( target, prop, receiver ) {
			if (prop === "push") {
				return function(value) {
					target.push(value);
					listener({ target, value, method: prop });
				}
			}
			if (prop === "pop") {
				return function() {
					const value = target.pop();
					listener({ target, value, method: prop });
					return value;
				}
			}
			if (prop === "shift") {
				return function() {
					const value = target.shift();
					listener({ target, value, method: prop });
					return value;
				}
			}
			if (prop === "unshift") {
				return function(value) {
					target.unshift(value);
					listener({ target, value, method: prop });
					return value;
				}
			}
			if (prop === "splice") {
				return function(...values) {
					target.splice(...values);
					listener({ target, value: values, method: prop });
				}
			}
			// default behavior of properties and methods
			return Reflect.get(target, prop, receiver);
		}
	}
 	return new Proxy(target, handler);
}

const target = [];

function listener(results) {
	console.log(results);
}

const list = createObservableArray({ target, listener });
list.push(1); // { "target": [ 1 ], "value": 1, "method": "push" }
list.push(2); // { "target": [ 1, 2 ], "value": 2, "method": "push" }
list.push(3); // { "target": [ 1, 2, 3 ], "value": 3, "method": "push" }
list.push(4); // { "target": [ 1, 2, 3, 4 ], "value": 4, "method": "push" }
list.push(5); // { "target": [ 1, 2, 3, 4, 5 ], "value": 5, "method": "push" }
list.push(6); // { "target": [ 1, 2, 3, 4, 5, 6 ], "value": 6, "method": "push" }
list.push(7); // { "target": [ 1, 2, 3, 4, 5, 6, 7 ], "value": 7, "method": "push" }
list.pop(); // { "target": [ 1, 2, 3, 4, 5, 6 ], "value": 7, "method": "pop" }
list.shift(); // { "target": [ 2, 3, 4, 5, 6 ], "value": 1, "method": "shift" }
list.unshift(2); // { "target": [ 2, 2, 3, 4, 5, 6 ], "value": 2, "method": "unshift" }
list.splice(4, 1, 9); // { "target": [ 2, 2, 3, 4, 9, 6 ], "value": [ 4, 1, 9 ], "method": "splice" }

This could make for an interesting way to track changes to an Array across parts of your application and react to those changes.

Summary

Native Proxies are a powerful tool in your JavaScript utility belt. You could fetch data in your Proxy handlers, validate data, or even have the Proxy sync directly to parts of the DOM if you wanted. There are a number of state management tools and frameworks that leverage Proxy under the hood. Some state management tools will even drill down to child objects you provide, and wrap those objects with Proxies to provide more fine-grained reactivity. However, if you don’t feel you have the need to add third-party libraries to manage state and observability in your application, you can rest easy knowing you can build your own flexible tooling with Proxies!