Skip to main content

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.