DEV Community

Cover image for 7 Use Cases for Javascript Proxies 🧙
Matt Lewandowski
Matt Lewandowski

Posted on

7 Use Cases for Javascript Proxies 🧙

JavaScript's Proxy object is a useful tool that opens up a world of possibilities, allowing you to create some really useful behaviors in your applications. When combined with TypeScript, Proxy enhances your ability to manage and manipulate objects and functions in ways you might not have thought possible. In this article, we'll explore the incredible utility of Proxies through practical examples.

What is a Proxy?

A Proxy in Javascript is a wrapper around another object (target), which lets you intercept and redefine fundamental operations for that object, such as property lookup, assignment, enumeration, and function invocation. This means you can add custom logic when getting or setting properties. This is great for handling validations, notifications, or even automatic data binding.

Creating a Simple Proxy

Let's jump in and see how to create a Proxy. We'll start with a really basic example, in case you haven't seen Proxies before.

type MessageObject = {
    message: string;
};

let target: MessageObject = {
    message: "Hello, world!"
};

let handler: ProxyHandler<MessageObject> = {
    get: (obj, prop) => {
        return `Property ${String(prop)} is: ${obj[prop]}`;
    }
};

let proxy: MessageObject = new Proxy(target, handler);
console.log(proxy.message);  // Output: Property message is: Hello, world!
Enter fullscreen mode Exit fullscreen mode

In this example, whenever a property on the proxy is accessed, the handler's get method is called, allowing us to modify the behavior of simply accessing a property. I'm sure you can imagine all the different possibilities this opens up.

Now let's get into 7 more useful examples!

1. Auto-populating Properties

Proxies can dynamically populate object properties when accessed, which is useful for on-demand processing or initialization of complex objects.

type LazyProfile = {
    firstName: string;
    lastName: string;
    fullName?: string;
};

let lazyProfileHandler = {
    get: (target: LazyProfile, property: keyof LazyProfile) => {
        if (property === "fullName" && !target[property]) {
            target[property] = `${target.firstName} ${target.lastName}`;
        }
        return target[property];
    }
};

let profile: LazyProfile = new Proxy({ firstName: "John", lastName: "Doe" }, lazyProfileHandler);
console.log(profile.fullName);  // Output: John Doe
Enter fullscreen mode Exit fullscreen mode

2. Operation Counting

Use a Proxy to count how many times certain operations are performed on an object. This is particularly useful for debugging, monitoring, or profiling application performance.

type Counter = {
    [key: string]: any;
    _getCount: number;
};

let countHandler = {
    get: (target: Counter, property: keyof Counter) => {
        if (property === "_getCount") {
            return target[property];
        }
        target._getCount++;
        return target[property];
    }
};

let counter: Counter = new Proxy({ a: 1, b: 2, _getCount: 0 }, countHandler);
counter.a;
counter.b;
console.log(counter._getCount);  // Output: 2
Enter fullscreen mode Exit fullscreen mode

3. Immutable Objects

Create truly immutable objects using Proxies by intercepting and preventing any changes to the object after its creation.

function createImmutable<T extends object>(obj: T): T {
    return new Proxy(obj, {
        set: () => {
            throw new Error("This object is immutable");
        }
    });
}

const immutableObject = createImmutable({ name: "Jane", age: 25 });
// immutableObject.age = 26;  // Throws error
Enter fullscreen mode Exit fullscreen mode

4. Method Chaining and Fluent Interfaces

Enhance method chaining by using Proxies to build fluent interfaces, where each method call returns a Proxy to enable further calls.

type FluentPerson = {
    setName(name: string): FluentPerson;
    setAge(age: number): FluentPerson;
    save(): void;
};

function FluentPerson(): FluentPerson {
    let person: any = {};

    return new Proxy({}, {
        get: (target, property) => {
            if (property === "save") {
                return () => { console.log(person); };
            }
            return (value: any) => {
                person[property] = value;
                return target;
            };
        }
    }) as FluentPerson;
}

const person = FluentPerson();
person.setName("Alice").setAge(30).save();  // Output: { setName: 'Alice', setAge: 30 }
Enter fullscreen mode Exit fullscreen mode

5. Smart Caching

This is one of my favorite use cases. Implement smart caching mechanisms where data is fetched or calculated on-demand and then stored for quick subsequent accesses.

function smartCache<T extends object>(obj: T, fetcher: (key: keyof T) => any): T {
    const cache: Partial<T> = {};
    return new Proxy(obj, {
        get: (target, property: keyof T) => {
            if (!cache[property]) {
                cache[property] = fetcher(property);
            }
            return cache[property];
        }
    });
}

const userData = smartCache({ userId: 1 }, (prop) => {
    console.log(`Fetching data for ${String(prop)}`);
    return { name: "Bob" };  // Simulated fetch
});

console.log(userData.userId);  // Output: Fetching data for userId, then returns { name: "Bob" }
Enter fullscreen mode Exit fullscreen mode

6. Dynamic Property Validation

Proxies can dynamically enforce rules for property assignments. Here's how you can ensure that certain conditions are met before properties are changed:

let user = {
  age: 25
};

let validator = {
  set: (obj, prop, value) => {
    if (prop === 'age' && (typeof value !== 'number' || value < 18)) {
      throw new Error("User must be at least 18 years old.");
    }
    obj[prop] = value;
    return true;  // Indicate success
  }
};

let userProxy = new Proxy(user, validator);
userProxy.age = 30;  // Works fine
console.log(userProxy.age);  // Output: 30
// userProxy.age = 'thirty';  // Throws error
// userProxy.age = 17;  // Throws error
Enter fullscreen mode Exit fullscreen mode

7. Watching for Changes

A common use case for Proxies is creating watchable objects that notify you when changes occur.

function onChange(obj, onChange) {
  const handler = {
    set: (target, property, value, receiver) => {
      onChange(`Property ${String(property)} changed to ${value}`);
      return Reflect.set(target, property, value, receiver);
    }
  };
  return new Proxy(obj, handler);
}

const person = { name: "John", age: 30 };
const watchedPerson = onChange(person, console.log);

watchedPerson.age = 31;  // Console: Property age changed to 31
Enter fullscreen mode Exit fullscreen mode

Downsides of Using Proxies

While Proxies are super useful, they come with a few caveats:

  1. Performance: Proxies can introduce a performance overhead, especially in high-frequency operations, because every operation on the proxy has to go through the handler.
  2. Complexity: With great power comes greater complexity. Incorrect use of Proxies can lead to hard-to-debug problems and maintainability issues.
  3. Compatibility: Proxies cannot be polyfilled for older browsers that do not support ES6 features, limiting their use in environments that require broad compatibility.

The End

Proxies in JavaScript, especially when used with TypeScript, offer a flexible way to interact with objects. They enable things like validation, observation, and bindings. Whether you're building complex user interfaces, developing games, or working on server-side logic, understanding and utilizing Proxies can provide you with a deeper level of control and sophistication in your code. Thanks for reading and I hope you learned something new! 🎓

Also, shameless plug 🔌. If you work in an agile dev team and use tools for your online meetings like planning poker or retrospectives, check out my free tool called Kollabe!

Top comments (11)

Collapse
 
matatbread profile image
Matt

Nice article.

Proxies also find a place in state management, specifically in some UI frameworks, which I guess is a specific case of number 7 above.

To avoid the issues you mention, AI-UI limits use of Proxies to specific cases of iterable objects that can't be implemented in any other way.

Collapse
 
joeattardi profile image
Joe Attardi

Proxies are cool! I recently used one with the XMLHttpRequest's open method (my project uses Axios). I needed global events for whenever a request started or completed, so I replaced XMLHttpRequest.prototype.open with a Proxy to the real implementation, and before calling the real implementation I add a couple of event listeners to the target (the XHR object).

I was going to blog about this but I dont think people are scrambling for content about XMLHttpRequest these days :)

Great article!

Collapse
 
appqui profile image
Igor Golodnitsky

Always curious why would anyone use Axios on browser? It looks like 50 kb of js, just to do the same as fetch.

Collapse
 
joeattardi profile image
Joe Attardi

It's a fair question.

One reason is compatibility - Axios uses XMLHttpRequest so it's good for situations where fetch isn't supported. That's rare now but a lot of legacy apps probably are using it. Once you have an established app using something like axios it's a lot of work to pull it out and replace with fetch.

It also provides some more advanced things that are trickier to do with fetch like request interceptors.

Collapse
 
taufik_nurrohman profile image
Taufik Nurrohman

FYI, this is similar to “magic methods” in PHP:

  • __call()
  • __callStatic()
  • __get()
  • __set()
  • __unset()
Collapse
 
laci556 profile image
László Bucsai

Great article! Proxies are definitely one of the most powerful features of JS. We use them to bridge the gap between the TS type system and concrete values, for example providing type-safe functions for creating string templates (translations, email templates etc). The function receives a callback whose argument is a Proxy of an empty object but acts like the type we're expecting to be passed to template. Then the accessed properties return template variables formatted for the given library. createTemplate((user) => `Hello ${user.name}`) returns "Hello {{ user.name }}", where the user object doesn't actually contain any data, but it makes creating these kinds of static values feel like writing dynamic JS while also providing type safety. (I don't even want to think about how many hours I've wasted debugging random undefineds because of small typos in handwritten templates 😄)

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
alxwnth profile image
Alex

Interesting article, thanks!

Collapse
 
herrynguyenvn profile image
Herry Nguyen 🇻🇳

Helpful article. thank bro.

Collapse
 
drfcozapata profile image
Francisco Zapata

GENIAL!!!
Gracias por compartirlo.
Bendiciones desde Venezuela

Collapse
 
mrvaa5eiym profile image
mrVAa5eiym

what is the difference between

obj[prop] = value;

return Reflect.set(target, property, value, receiver);

why use one or the other in the cases above?

Some comments may only be visible to logged-in visitors. Sign in to view all comments.