Using The Proxy Design Pattern with React

Explore the usage of the Proxy Design Pattern in a React environment

Danusha Navod
Bits and Pieces

--

As developers, you’ve undoubtedly encountered the term ‘Design Patterns.’ This isn’t just a word to glance over; it’s a key to unlocking a realm of coding mastery. These varied and diverse patterns serve as valuable tools in software development, enhancing code organization, maintainability, and scalability.

Among the other design patterns out there, the Proxy Design Pattern, introduced in 1995 as a Structural Design Pattern, has since found widespread application in various scenarios.

In this article, we dive deeper into Proxy Design Patterns, exploring their usages, and let’s uncover how to apply these patterns to your future React applications, enhancing their structure and functionality.

Understanding the Proxy Design Pattern

Source: https://refactoring.guru/design-patterns/proxy

The Proxy Design Pattern, lets you provide a substitute or placeholder for another object.

By doing so, the proxy gains control over access to the original object, enabling the execution of actions before or after the request reaches the original object.

Why do you want to control the access to an object?

For example, you might have encountered situations where certain functions operate continuously, creating massive objects consuming heavy system and network resources. You may need to run this periodically or under some conditions but continuous running can harm the overall performance of the code.

In such cases, the Proxy Design Pattern becomes valuable. This involves building another proxy class mirroring the original service object’s interface.

What is the benefit of having this?

By implementing a proxy object, you can execute some other logic before or after executing the main logic of the class.

For instance, in the example scenario mentioned earlier, you can set up a proxy object to cache data handled by the main object. This proactive caching minimizes the repetition of expensive computations, improving performance and resource efficiency.

Not only for data caching but proxy design patterns can also be beneficial in many ways, including,

  • Improving security: These can enforce security measures, such as authentication and authorization checks, ensuring that sensitive data or operations are accessible only to authorized users.
  • Reduced Resource Consumption: Lazy loading, caching, and controlled access through proxies can reduce the resource consumption of the application.
  • Network Optimization: In distributed systems, proxy design patterns can optimize network communication by acting as placeholders for remote objects. This aids in minimizing the impact of network latency.
  • Concurrency Control: Proxies can implement mechanisms for controlling concurrent access to shared resources, preventing race conditions, and ensuring data consistency.

What is the Proxy Object in JavaScript

JavaScript’s Proxy Object has been constructed based on the core concept of proxies in other programming languages, but it comes with its own set of features tailored to the dynamic nature of JavaScript.

Unlike traditional proxy objects in other languages, JavaScript’s Proxy object provides a highly flexible and dynamic way to interact with and manipulate objects.

In JavaScript a Proxy object can be defined as follows:

const proxy = new Proxy(target, handler);

The target refers to the object that the proxy wraps around, while the handler is an object containing one or more "traps" — functions designed to intercept and modify operations carried out on the target object.

Example usage:

const targetObject = {
message: "Hello, Proxy!",
};

const handler = {
get: function (target, prop) {
console.log(`Accessing property: ${prop}`);
return target[prop];
},
};

const proxy = new Proxy(targetObject, handler);
console.log(proxy.message);

// Accessing property: message
// Hello, Proxy!

In this example, I used the get trap to demonstrate a basic usage of the JS Proxy Object. You can find more from here.

Having a basic knowledge of JS proxy objects will be helpful when understanding the next section.

How to Use The Proxy Design Pattern in React?

If you wish to directly explore the code, feel free to check out this Bit Scope.

Now, you should have a clear idea about the proxy design pattern and the proxy object in JavaScript. It’s time to get your hands dirty and gain hands-on experience with React using Proxy.

React comes with its built-in features set to optimize the performance and overall responsiveness of the app. However, when combining the proxy design pattern with these features, you can further enhance the performance of the app with well-controlled object interactions and data access.

Enhanced Security

Proxy design patterns can be helpful in a React application to enhance security by providing layers of control over object interactions and access to sensitive data. Below, you can find a simple demonstration of this concept.

import { useEffect, useState } from 'react';

const fetchUserData = () => {
// Place the network request here
console.log('Data fetching...');

return {
username: 'john_doe',
password: 'user_password',
};
};

const useSecureUserData = () => {
const [userData, setUserData] = useState(null);

const isUserAdmin = () => {
// Implement your authorization logic here
// For simplicity, assume logged user is admin
return true;
};

useEffect(() => {
const userProxy = new Proxy(fetchUserData(), {
get(target, prop) {
// Check if the user is authorized to access the user password
if (prop === 'password' && !isUserAdmin()) {
console.warn('Unauthorized access to password!');
return null; // Return null or handle unauthorized access
}

return target[prop];
},
});

setUserData(userProxy);
}, []);

return userData;
};

export const UserProfile = () => {
const userData = useSecureUserData();

if (!userData) {
return <div>Loading...</div>;
}

return (
<div>
<p>Username: {userData.username}</p>
<p>Password: {userData.password}</p>
</div>
);
};

In this simple example, the useSecureUserData custom hook fetches the user data and creates a proxy object to validate user data before returning it. Here the fetchUserData returns the real object and userProxy is the Proxy object. Finally UserProfile component works as the client calling the proxy object.

While this example illustrates using a Proxy object for access management, it’s important to note that using Higher-Order Components (HOC), the Context API, or router guards are common and sometimes more conventional approaches in React. The choice depends on the specific requirements of your application and the desired level of access control.

Efficient Data Management

If your application involves heavy network requests that consume resources and slow down performance, caching fetched data can be a viable solution. There are lots of solutions out there for caching data in React, and using a proxy object is one of them.

In this next example, we will construct a proxy object to cache the network response and reuse the data without making additional network requests.

import React from 'react';

const fetchUserData = () => {
// Place the network request here
console.log('Data fetching...');

return [
{
id: '0001',
name: 'John Doe',
email: 'john@mail.com',
},
];
};

const cachedUserData = new Proxy([], {
get: function (target: any) {
if (!target.data) {
// Fetch data if it's not cached
target.data = fetchUserData();
} else {
console.log('Returning cached data...');
}

return target.data;
},
});

export function UserList() {
const [state, setState] = React.useState(
[] as { id: string; name: string; email: string }[]
);

return (
<div>
{state.map((user) => (
<div key={user.id}>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</div>
))}
<button onClick={() => setState(cachedUserData.data)}>Click me</button>
</div>
);
}

Here, the API call won’t be sent if the data is already in the object. Otherwise, it fetches the data and caches it using the proxy object. Try clicking on the “Click me” button here and check the console to get a better idea of how the caching works here.

For a simple scenario like this, managing data states effectively and avoiding network requests in every render can mitigate performance issues in the application. Not only for a scenario like this, but Proxy data objects can also be helpful in many ways to cache data in your application.

Take a look at the following code:

const createExpensiveFunction = () => {
// Replace this with your actual expensive function
const expensiveFunction = (arg) => {
console.log(`Calculating expensive value for ${arg}`);

return arg * 2;
};

const cache = new Map();

const handler = {
apply(target, thisArg, argumentsList) {
const arg = argumentsList[0];

if (!cache.has(arg)) {
cache.set(arg, expensiveFunction(arg));
} else {
console.log('Returning cached data...');
}

return cache.get(arg);
},
};

return new Proxy(expensiveFunction, handler);
};

const memorizedFunction = createExpensiveFunction();

export const MemorizeExpensiveFunction = () => {
const items = [1, 2, 1, 1, 5];

return (
<div>
{items.map((item, index) => (
<div key={`${item}-${index}`}>
<p>Item: {item}</p>
<p>Expensive Value: {memorizedFunction(item)}</p>
</div>
))}
</div>
);
};

This example illustrates a straightforward method for caching expensive function results. The createExpensiveFunction returns the Proxy object. It checks the input arguments, and if the function is called with the same inputs again, it retrieves the cached result. Otherwise, it executes the expensiveFunction and stores the result in the cache.

Other common use cases

Not only these mentioned ones but there are a lot of use cases out there in using proxy design patterns that can be beneficial to your development including,

  • Abstraction and code reusability: Proxies can simplify, reduce code duplication, and enhance the reusability of the code by abstracting complex data access patterns or API interactions.
  • Data validation and sanitization: Proxies can be used to validate user inputs before reaching the component’s logic. This can also help sanitize data, guarding against XSS attacks and enhancing overall application security.
  • Performance monitoring and optimization: Proxies can be used to monitor performance metrics like API call durations or data access patterns, enabling developers to pinpoint and address performance bottlenecks.

Take a look at this Bit Scope for more coding examples related to the mentioned use cases. Feel free to try, customize, and experiment with it to gain a deeper understanding of the concept.

Concluding Thoughts

The Proxy design pattern, coupled with JavaScript’s Proxy object, offers a powerful toolkit for enhancing React applications. From optimizing performance and securing sensitive data to efficient data management and beyond, proxies provide a versatile solution.

While we explored practical use cases, remember that the ideal solution varies based on specific application requirements. React empowers developers with flexibility, allowing them to choose the best approach.

As you explore Proxy design patterns, remember their power: streamlined code, improved security, and optimized performance for a resilient React development experience.

If you wish to explore the code we’ve implemented, explore this Bit Scope.

Thank you for reading.

--

--