How Does JavaScript Proxy Enable Reactive State Management?
I spent an entire afternoon wrestling with Redux boilerplate. Three files just to increment a counter. Action types, action creators, reducers, dispatchers—and I still forgot to call the update function after changing state. The UI sat there, frozen, while my console logged the correct value.
That’s when I discovered JavaScript’s Proxy API. It’s the same mechanism Vue 3 and SolidJS use for their core reactivity systems. And it’s built into the browser.
The Problem with Plain Objects
Plain JavaScript objects don’t notify anyone when properties change. I had this code:
const state = { count: 0};
function updateUI() { document.getElementById('counter').textContent = state.count;}
state.count = 5; // UI doesn't update!I forgot to call updateUI() after every mutation. Then I added more properties, more update calls scattered everywhere, and the code became a mess of manual synchronization.
Redux promised to solve this with its unidirectional flow. But for my simple app, it felt like using a sledgehammer to hang a picture frame.
Enter JavaScript Proxy
The Proxy API lets you intercept operations on objects. It wraps your target object and intercepts operations through “trap” handlers:
const target = { count: 0 };
const handler = { get(target, property) { console.log(`Reading ${property}`); return target[property]; }, set(target, property, value) { console.log(`Setting ${property} to ${value}`); target[property] = value; return true; // Required! }};
const proxy = new Proxy(target, handler);
proxy.count; // Logs: "Reading count"proxy.count = 5; // Logs: "Setting count to 5"The key insight: I could automatically trigger UI updates in the set trap without any manual calls.
My First Reactive Implementation
I tried building a simple reactive wrapper:
function reactive(obj) { return new Proxy(obj, { get(target, key) { return target[key]; }, set(target, key, value) { target[key] = value; updateUI(); // Auto-update! return true; } });}
const state = reactive({ count: 0 });state.count = 5; // UI updates automaticallyIt worked! But I had hardcoded updateUI(). What if different components needed to react to different properties?
Adding Dependency Tracking
I realized I needed to track which components cared about which properties. Vue calls this “dependency tracking.” The get trap records who’s reading, and the set trap notifies them:
const subscribers = new Map();
function track(target, key) { // In a real implementation, this would track // which component is currently running // and subscribe it to this property}
function trigger(target, key) { const callbacks = subscribers.get(key); if (callbacks) { callbacks.forEach(cb => cb()); }}
function reactive(obj) { return new Proxy(obj, { get(target, key) { track(target, key); // Record dependency return target[key]; }, set(target, key, value) { target[key] = value; trigger(target, key); // Notify subscribers return true; } });}This pattern—track on read, trigger on write—is exactly how Vue 3 works under the hood.
Common Mistakes I Made
Mistake 1: Destructuring Breaks Reactivity
I tried to clean up my code with destructuring:
// WRONG: Breaks reactivityconst { count } = state;count = 5; // Doesn't trigger proxy
// CORRECT: Keep proxy referencestate.count = 5;The destructured variable is just a copy of the value, not a reference to the proxy. The proxy trap never fires.
Mistake 2: Forgetting return true
My first implementation threw a TypeError in strict mode:
// WRONG: Throws TypeErrorconst handler = { set(target, prop, value) { target[prop] = value; // Missing return true! }};
// CORRECTconst handler = { set(target, prop, value) { target[prop] = value; return true; // Required for success }};The set trap must return true to indicate success. Otherwise, JavaScript assumes the assignment failed.
Mistake 3: Proxy Identity Confusion
I compared a proxy with its original object and got unexpected results:
const raw = { count: 0 };const proxy = reactive(raw);
console.log(proxy === raw); // false!A proxy is a different object than its target. This matters when you use object identity in comparisons or as Map/WeakMap keys.
Building a Complete Reactive Store
I wanted something more reusable. Here’s what I ended up with:
class ReactiveStore { constructor(initialState) { this.subscribers = new Map(); this.state = this.createProxy(initialState); }
createProxy(obj) { // Handle nested objects recursively if (typeof obj !== 'object' || obj === null) { return obj; }
for (const key in obj) { if (typeof obj[key] === 'object' && obj[key] !== null) { obj[key] = this.createProxy(obj[key]); } }
return new Proxy(obj, { get: (target, prop) => { this.track(prop); return target[prop]; }, set: (target, prop, value) => { // Handle nested proxies if (typeof value === 'object' && value !== null) { value = this.createProxy(value); } target[prop] = value; this.notify(prop); return true; } }); }
subscribe(property, callback) { if (!this.subscribers.has(property)) { this.subscribers.set(property, new Set()); } this.subscribers.get(property).add(callback);
// Return unsubscribe function return () => { this.subscribers.get(property).delete(callback); }; }
track(property) { // In a real implementation, track the active component // For now, this is a placeholder }
notify(property) { const callbacks = this.subscribers.get(property); if (callbacks) { callbacks.forEach(cb => cb(this.state[property])); } }}Usage is straightforward:
const store = new ReactiveStore({ count: 0, user: { name: 'Alice' }});
// Subscribe to changesstore.subscribe('count', (value) => { document.getElementById('counter').textContent = value;});
// Update triggers subscriber automaticallystore.state.count = 5; // DOM updatesAdding Validation
One thing Redux never gave me easily was validation. With Proxy, it’s trivial:
const validatedState = new Proxy( { email: '', age: 0 }, { set(target, prop, value) { if (prop === 'email' && value && !value.includes('@')) { throw new Error('Invalid email format'); } if (prop === 'age' && (value < 0 || value > 150)) { throw new Error('Age must be between 0 and 150'); } target[prop] = value; return true; } });
validatedState.email = 'invalid'; // throws ErrorInvalid values never make it into state. The throw happens before assignment.
When to Use Proxy vs. Redux
I still use Redux for complex apps. But for many cases, Proxy is enough:
| Scenario | Proxy Works Well | Stick with Redux |
|---|---|---|
| Simple state | Yes | Overkill |
| Validation | Built-in | Manual middleware |
| Learning curve | Low | High |
| Time-travel debug | No | Yes |
| Middleware ecosystem | None | Rich |
| Team familiarity | Varies | Established |
Proxy excels when you need lightweight reactivity without the overhead of actions, reducers, and dispatchers. Redux wins when you need centralized state with middleware, time-travel debugging, and a mature ecosystem.
How Vue 3 Uses This Pattern
Vue 3 switched from Object.defineProperty (Vue 2) to Proxy for its reactivity system. The core implementation looks like this:
function reactive(obj) { return new Proxy(obj, { get(target, key, receiver) { track(target, key); const result = Reflect.get(target, key, receiver); // Deep reactivity: make nested objects reactive too if (typeof result === 'object' && result !== null) { return reactive(result); } return result; }, set(target, key, value, receiver) { const oldValue = target[key]; const result = Reflect.set(target, key, value, receiver); if (oldValue !== value) { trigger(target, key); } return result; } });}Notice the use of Reflect methods. This ensures the this context works correctly for inherited properties and accessor methods.
SolidJS Takes It Further
SolidJS uses Proxies for fine-grained reactivity at the property level:
import { createStore } from "solid-js/store";
const [store, setStore] = createStore({ users: [ { id: 0, name: "Alice", active: false }, { id: 1, name: "Bob", active: true } ]});
// Only this specific property is trackedconsole.log(store.users[0].name);
// Update with path syntaxsetStore("users", 0, "active", true);SolidJS doesn’t use Virtual DOM. The Proxy mechanism tracks exactly which DOM nodes depend on which properties, enabling surgical updates.
React Integration
I tried combining Proxy with React’s Context:
import React, { createContext, useContext, useState, useMemo } from 'react';
const createProxyState = (initialState, setState) => { return new Proxy(initialState, { set(target, property, value) { target[property] = value; setState({ ...target }); return true; } });};
const StateContext = createContext(null);
export const StateProvider = ({ children, initialState }) => { const [state, setState] = useState(initialState); const proxyState = useMemo( () => createProxyState(state, setState), [state] );
return ( <StateContext.Provider value={proxyState}> {children} </StateContext.Provider> );};
export const useGlobalState = () => useContext(StateContext);This lets components mutate state directly without dispatchers:
const Counter = () => { const state = useGlobalState(); return ( <button onClick={() => state.count++}> Count: {state.count} </button> );};Be careful with this pattern—React’s reconciliation model doesn’t naturally align with Proxy-based reactivity. It works but may cause unnecessary re-renders if not optimized.
Final Thoughts
JavaScript Proxy transformed my approach to state management. Instead of writing boilerplate to connect state changes to UI updates, I let the proxy handle it automatically. The same technique powers Vue 3 and SolidJS.
For simple to medium complexity apps, Proxy-based state management is often enough. No dependencies, minimal code, and native browser support. The next time you reach for Redux, ask yourself: do I really need all that machinery, or can a simple proxy do the job?
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Vue.js Reactivity in Depth
- 👨💻 SolidJS Stores Documentation
- 👨💻 MDN Proxy API Reference
- 👨💻 Building a Reactive State Manager with Proxies
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments