Why SoulState Avoids Proxies
Modern state management libraries often leverage JavaScript Proxies for automatic dependency tracking. While powerful, SoulState deliberately chooses a different path based on explicit selectors and immutable updates. This decision is rooted in our philosophy of predictable performance, explicit control, and broad compatibility.
The Proxy Approach vs SoulState's Approach
// ❌ Proxy-based libraries (automatic tracking)
const state = proxy({ user: { name: 'John' }, posts: [] });
// Component automatically tracks accessed properties
function UserComponent() {
const userName = useSnapshot(state).user.name;
// Re-renders when state.user.name changes
}
// ✅ SoulState (explicit selectors)
const store = createStore({ user: { name: 'John' }, posts: [] });
// You explicitly define dependencies
function UserComponent() {
const userName = useStore(store, state => state.user.name);
// Only re-renders when state.user.name changes
}
Performance: Predictable vs "Magic"
SoulState's Predictable Performance
// O(1) subscription management with linked list
interface SubscriptionNode<T, S> {
selector: (state: T) => S;
listener: (selectedState: S, prevSelectedState: S) => void;
equalityFn: (a: S, b: S) => boolean; // Usually Object.is (O(1))
lastState: S;
prev: SubscriptionNode<T, any> | null; // O(1) removal
next: SubscriptionNode<T, any> | null; // O(1) addition
}
Proxy Performance Concerns
- Hidden traversal costs when accessing nested properties
- Memory overhead from maintaining dependency graphs
- Unpredictable re-renders from implicit tracking
Real Implementation
SoulState uses a doubly linked list in src/core/subscriptions.ts for O(1) subscription management, avoiding the overhead of proxy dependency graphs.
Debugging: Explicit vs Implicit
SoulState: Clear Dependency Trail
// Easy to debug - dependencies are explicit
const user = useStore(store, state => state.user);
const posts = useStore(store, state => state.posts);
const isLoading = useStore(store, state => state.isLoading);
// Console shows exactly what changed:
// 🔄 Subscription: {user} → {newUser}
// 🔄 Subscription: {posts} → {newPosts}
// ✅ No update for isLoading (unchanged)
Proxy-based: Hidden Dependencies
// Hard to debug - dependencies are implicit
const { user, posts, isLoading } = useSnapshot(state);
// Why did it re-render? Which property changed?
// Debugging requires digging into proxy internals
Compatibility & Serialization
SoulState: Universal Compatibility
// Works everywhere JavaScript works
const store = createStore(initialState);
// Easy serialization for persistence
localStorage.setItem('state', JSON.stringify(store.get()));
// Works with Web Workers
worker.postMessage(store.get());
Proxy Limitations
const state = proxy({ user: 'John' });
// ❌ Proxies aren't serializable
JSON.stringify(state); // "{}" - loses data!
// ❌ Compatibility issues in older environments
// ❌ Some JavaScript runtimes have limited Proxy support
The SoulState Alternative: Optimized Immutability
Instead of proxies, SoulState combines three optimized techniques:
1. Structural Sharing Optimization
// In src/core/store.ts - minimizes object creation
let hasChanged = false;
const updatedKeys = Object.keys(partialState);
for (let i = 0; i < updatedKeys.length; i++) {
const key = updatedKeys[i] as keyof T;
if (!Object.is(state[key], (partialState as T)[key])) {
hasChanged = true;
break;
}
}
// Only create new object if something actually changed
if (!hasChanged) return;
const nextState = { ...state, ...(partialState as Partial<T>) };
2. Microtask Batching
// Batches multiple updates into single notification
let isNotificationScheduled = false;
const scheduleNotification = () => {
if (!isNotificationScheduled) {
isNotificationScheduled = true;
queueMicrotask(notifySubscribers); // Efficient batching
}
};
3. Surgical Selector Subscriptions
// Each component subscribes only to needed data
function UserProfile() {
const name = useStore(store, state => state.user.name); // ✅
const email = useStore(store, state => state.user.email); // ✅
// ❌ Never re-renders when posts change
}
Real-World Impact
Bundle Size Comparison
// SoulState core: ~2KB gzipped
// Proxy-based solutions: ~5-10KB+ (proxy + dependency tracking)
// Better tree-shaking with explicit imports
import { createStore, useStore } from 'soulstate';
import { shallow } from 'soulstate/utils';
Performance in Large Apps
// With 1000+ components:
// SoulState: O(1) subscription management scales linearly
// Proxy-based: Dependency graph maintenance can become O(n²)
The SoulState Advantage
By avoiding proxies, SoulState delivers predictable O(1) performance, explicit debugging trails, universal compatibility, and minimal bundle size—without sacrificing the fine-grained updates that make modern state management powerful.
When Proxies Make Sense
Proxies are excellent for:
- Developer tools and debugging utilities
- Mutable APIs where convenience outweighs performance needs
- Prototyping where rapid iteration is key
But for production state management where performance, predictability, and compatibility matter most, SoulState's explicit selector approach proves superior.
Conclusion
SoulState's no-proxy architecture isn't about rejecting modern features—it's about choosing the right tool for the job. For building fast, predictable, and scalable applications, explicit selectors with optimized immutable updates provide a better foundation than implicit proxy magic.
The result is a state management solution that's fast by default, clear by design, and reliable everywhere JavaScript runs.