DEV Community

Cover image for 4. Proxies in the Wild: Real-World JavaScript Wizardry That Actually Works
Sandheep Kumar Patro
Sandheep Kumar Patro

Posted on

4. Proxies in the Wild: Real-World JavaScript Wizardry That Actually Works

Proxies in Real-World Use Cases

In our previous articles, we explored the fundamentals of JavaScript Proxies and advanced patterns for various data structures. Now, let's dive into how these patterns are applied in real-world applications and frameworks. Understanding these practical implementations will help you leverage Proxies more effectively in your own projects.

Reactivity in Frontend Frameworks

Modern frontend frameworks rely heavily on detecting changes to data in order to update the UI accordingly. Proxies have revolutionized how this reactivity is implemented.

How Vue.js Uses Proxies

Vue 3 completely rewrote its reactivity system to use Proxies instead of the previous Object.defineProperty() approach. This change brought significant improvements in both performance and capabilities.

Here's a simplified example of how Vue's reactivity system works:

// Simplified version of Vue 3's reactivity system
function reactive(target) {
  const handlers = {
    get(target, property, receiver) {
      const result = Reflect.get(target, property, receiver);

      // Track that this property was accessed during a component render
      track(target, property);

      // Recursively make nested objects reactive
      if (typeof result === 'object' && result !== null) {
        return reactive(result);
      }

      return result;
    },

    set(target, property, value, receiver) {
      const oldValue = target[property];
      const result = Reflect.set(target, property, value, receiver);

      if (oldValue !== value) {
        // Notify all components that depend on this property
        trigger(target, property);
      }

      return result;
    }
  };

  return new Proxy(target, handlers);
}

// Simplified implementation
let activeEffect = null;
const targetMap = new WeakMap();

function track(target, property) {
  if (!activeEffect) return;

  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }

  let dep = depsMap.get(property);
  if (!dep) {
    depsMap.set(property, (dep = new Set()));
  }

  dep.add(activeEffect);
}

function trigger(target, property) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;

  const effects = depsMap.get(property);
  if (effects) {
    effects.forEach(effect => effect());
  }
}

function watchEffect(effect) {
  activeEffect = effect;
  effect();  // Run once to track dependencies
  activeEffect = null;
}

// Usage in a component-like context
const state = reactive({
  count: 0,
  user: {
    name: 'Morgan',
    isAdmin: false
  }
});

// This would be similar to a component's render function
watchEffect(() => {
  console.log(`Count is: ${state.count}`);
  console.log(`User is: ${state.user.name}`);
});

// These updates will trigger the effect to run again
state.count++;  // "Count is: 1", "User is: Morgan"
state.user.name = 'Taylor';  // "Count is: 1", "User is: Taylor"
Enter fullscreen mode Exit fullscreen mode

This system elegantly tracks which components depend on which properties and only re-renders components when their dependencies change. The advantages over the old approach include:

  1. Complete tracking — All property additions/deletions are detected automatically
  2. Array support — Methods like push/pop work out of the box
  3. Nested reactivity — Complex nested objects are reactive with lower overhead

Building a Simple Reactive State System

Let's create our own simplified reactive state system inspired by these concepts:

function createReactiveState(initialState) {
  const subscribers = new Set();
  const state = new Proxy(initialState, {
    get(target, property, receiver) {
      const value = Reflect.get(target, property, receiver);

      // Make nested objects reactive too
      if (typeof value === 'object' && value !== null) {
        return new Proxy(value, this);
      }

      return value;
    },

    set(target, property, value, receiver) {
      const oldValue = target[property];
      const result = Reflect.set(target, property, value, receiver);

      if (oldValue !== value) {
        // Notify all subscribers
        subscribers.forEach(callback => callback({
          property,
          oldValue,
          newValue: value
        }));
      }

      return result;
    }
  });

  // Methods to subscribe/unsubscribe
  function subscribe(callback) {
    subscribers.add(callback);
    return () => subscribers.delete(callback);
  }

  return {
    state,
    subscribe
  };
}

// Example usage with a UI component
const { state, subscribe } = createReactiveState({
  todos: [
    { id: 1, text: 'Learn JavaScript', completed: true },
    { id: 2, text: 'Master Proxies', completed: false }
  ],
  filter: 'all'
});

// Simple rendering function
function renderTodoList() {
  const todos = state.todos.filter(todo => {
    if (state.filter === 'completed') return todo.completed;
    if (state.filter === 'active') return !todo.completed;
    return true;
  });

  console.log('Rendering todos:', todos);
  console.log('Active filter:', state.filter);
}

// Subscribe to state changes
const unsubscribe = subscribe(change => {
  console.log('State changed:', change);
  renderTodoList();
});

// Initial render
renderTodoList();

// These actions will trigger re-renders
state.todos.push({ id: 3, text: 'Build an app', completed: false });
state.filter = 'active';
state.todos[1].completed = true;
Enter fullscreen mode Exit fullscreen mode

This pattern forms the foundation of state management libraries like MobX and Solid.js, enabling a reactive programming model that's both powerful and intuitive.

Proxies for Virtual DOM & Lazy Rendering

Another important application of Proxies is in optimizing rendering performance through Virtual DOM implementations and lazy rendering strategies.

How React's Virtual DOM Works

While React doesn't directly use Proxies for its Virtual DOM (it predates their widespread adoption), the concept is similar. However, we can demonstrate how Proxies could enable a similar pattern:

function createVirtualElement(type, props = {}, ...children) {
  return {
    type,
    props: { ...props, children },
    _isVirtual: true
  };
}

function createElementProxy() {
  // Cache for already created virtual elements
  const cache = new Map();

  // Handler for our proxy
  const handler = {
    get(target, property) {
      return (...args) => {
        // Create a unique key for this element and its props
        const key = `${property}-${JSON.stringify(args)}`;

        // Return cached version if it exists
        if (cache.has(key)) {
          return cache.get(key);
        }

        // Create new virtual element
        const element = createVirtualElement(property, ...args);
        cache.set(key, element);
        return element;
      };
    }
  };

  return new Proxy({}, handler);
}

// Usage example
const h = createElementProxy();

function renderApp(state) {
  return h.div({ className: 'app' },
    h.h1({}, 'Todo List'),
    h.ul({ className: 'todo-list' },
      state.todos.map(todo => 
        h.li({ 
          key: todo.id,
          className: todo.completed ? 'completed' : ''
        }, todo.text)
      )
    )
  );
}

// This would be memoized across renders when props don't change
const vdom = renderApp({
  todos: [
    { id: 1, text: 'Learn JavaScript', completed: true },
    { id: 2, text: 'Master Proxies', completed: false }
  ]
});
Enter fullscreen mode Exit fullscreen mode

Using Proxies for Lazy Component Rendering

Proxies can improve performance by delaying component rendering until they're actually visible:

function createLazyComponent(componentFactory) {
  let component = null;
  let isRendered = false;

  return new Proxy({}, {
    get(target, property) {
      // Initialize the component when first accessed
      if (!component) {
        console.log('Lazy-loading component...');
        component = componentFactory();
        isRendered = true;
      }

      return component[property];
    }
  });
}

// Usage example
const HeavyChart = createLazyComponent(() => {
  console.log('Actually creating chart component (expensive operation)');

  // Simulate expensive component creation
  return {
    render() {
      return '<div class="chart">Complex Chart</div>';
    },

    update(data) {
      console.log('Updating chart with:', data);
    }
  };
});

// Component isn't created until first used
console.log('Component created but not initialized yet');

// Later, when the component becomes visible
console.log(HeavyChart.render());
// Logs:
// "Lazy-loading component..."
// "Actually creating chart component (expensive operation)"
// "<div class="chart">Complex Chart</div>"
Enter fullscreen mode Exit fullscreen mode

This pattern is especially useful for complex applications with many components that aren't all visible at once.

Generating Dynamic API Clients with Proxies

One of the most elegant applications of Proxies is creating dynamic API clients that automatically generate requests based on property access patterns.

Auto-generating API Calls

function createApiClient(baseUrl) {
  const handler = {
    get(target, property) {
      // Handle nested paths
      if (property === 'path') {
        return (path) => new Proxy({ _path: `${target._path || ''}/${path}` }, handler);
      }

      // Handle HTTP methods
      if (['get', 'post', 'put', 'delete'].includes(property)) {
        return async (data) => {
          const url = `${baseUrl}${target._path || ''}`;
          console.log(`Making ${property.toUpperCase()} request to ${url}`);

          // In a real implementation, this would use fetch
          return { url, method: property, data };
        };
      }

      // Create a new path segment
      return new Proxy({ _path: `${target._path || ''}/${property}` }, handler);
    }
  };

  return new Proxy({ _path: '' }, handler);
}

// Usage example
const api = createApiClient('https://api.example.com');

// These method chains are generated dynamically
api.users.get()
  .then(response => console.log('All users:', response));
// "Making GET request to https://api.example.com/users"

api.users[123].posts.get()
  .then(response => console.log('User posts:', response));
// "Making GET request to https://api.example.com/users/123/posts"

api.path('products').path('featured').get()
  .then(response => console.log('Featured products:', response));
// "Making GET request to https://api.example.com/products/featured"

api.comments.post({ text: 'Great article!' })
  .then(response => console.log('Posted comment:', response));
// "Making POST request to https://api.example.com/comments"
Enter fullscreen mode Exit fullscreen mode

This pattern creates an intuitive, chainable API that mirrors your backend routes without requiring explicit method definitions for each endpoint.

Handling RESTful Methods Dynamically

We can extend this pattern to support more RESTful conventions:

function createRestClient(baseUrl) {
  function createProxy(path = '') {
    return new Proxy(function() {}, {
      // Support function calls like api.users() to get all users
      apply(target, thisArg, args) {
        const id = args[0];
        return id !== undefined
          ? createProxy(`${path}/${id}`) // api.users(123)
          : { get: async () => ({ url: `${baseUrl}${path}`, method: 'GET' }) }; // api.users()
      },

      // Support property access like api.users
      get(target, property) {
        // HTTP methods
        if (['get', 'post', 'put', 'delete', 'patch'].includes(property)) {
          return async (data) => {
            const url = `${baseUrl}${path}`;
            console.log(`Making ${property.toUpperCase()} request to ${url}`);
            return { url, method: property.toUpperCase(), data };
          };
        }

        // Resource paths
        return createProxy(`${path}/${property}`);
      }
    });
  }

  return createProxy();
}

// Usage
const api = createRestClient('https://api.example.com');

// Different ways to access the same resource
api.users.get()                   // GET /users
api.users(123).get()              // GET /users/123
api.users[123].get()              // GET /users/123
api.users(123).posts.get()        // GET /users/123/posts
api.users(123).posts.post({text: 'Hello'})  // POST /users/123/posts
Enter fullscreen mode Exit fullscreen mode

This pattern is used in libraries like axios-proxy-api and similar tools to create fluent, intuitive API clients with minimal boilerplate.

Why These Patterns Matter

These real-world applications of Proxies demonstrate their transformative power:

  1. Cleaner APIs — Proxies enable intuitive interfaces that hide implementation complexity
  2. Performance — Lazy loading and caching patterns improve application responsiveness
  3. Developer Experience — Less boilerplate means more productive development
  4. Flexibility — Dynamic behavior adapts to changing requirements without code changes

The impact on modern JavaScript frameworks has been substantial—Vue 3's complete rewrite around Proxy-based reactivity led to significant performance improvements and a better developer experience.

Top comments (0)