Skip to content

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:

plain-object-fail.js
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:

basic-proxy.js
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:

first-reactive.js
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 automatically

It 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:

reactive-with-tracking.js
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:

destructuring-fail.js
// WRONG: Breaks reactivity
const { count } = state;
count = 5; // Doesn't trigger proxy
// CORRECT: Keep proxy reference
state.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:

return-true-required.js
// WRONG: Throws TypeError
const handler = {
set(target, prop, value) {
target[prop] = value;
// Missing return true!
}
};
// CORRECT
const 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:

proxy-identity.js
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:

reactive-store.js
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:

store-usage.js
const store = new ReactiveStore({
count: 0,
user: { name: 'Alice' }
});
// Subscribe to changes
store.subscribe('count', (value) => {
document.getElementById('counter').textContent = value;
});
// Update triggers subscriber automatically
store.state.count = 5; // DOM updates

Adding Validation

One thing Redux never gave me easily was validation. With Proxy, it’s trivial:

validated-state.js
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 = '[email protected]'; // OK
validatedState.email = 'invalid'; // throws Error

Invalid 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:

ScenarioProxy Works WellStick with Redux
Simple stateYesOverkill
ValidationBuilt-inManual middleware
Learning curveLowHigh
Time-travel debugNoYes
Middleware ecosystemNoneRich
Team familiarityVariesEstablished

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:

vue3-reactivity-simplified.js
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:

solidjs-style.js
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 tracked
console.log(store.users[0].name);
// Update with path syntax
setStore("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:

react-proxy-context.jsx
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:

counter-component.jsx
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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments