Selectors & Dependency Tracking
Selectors are the primary mechanism for reading data from a SoulState store. In SoulState, selectors are not just passive functions; they are the entry point for Automatic Dependency Tracking.
Automatic Tracking Proxy
When a selector runs (via useStore or computed), SoulState executes it using a Reusable Tracking Proxy. This proxy intercepts property accesses and records which keys your selector depends on.
const name = useStore(store, s => s.user.name);
// 1. Selector runs
// 2. Proxy detects access to 'user'
// 3. Node is registered in the Invalidation Graph for the 'user' key
The Recomputation Flow
SoulState's engine ensures that selectors only re-execute when necessary:
- State Update:
setState({ count: 1 })is called. - Invalidation: The
InvalidationGraphidentifies all nodes depending oncount. - Topological Sort: Affected nodes are sorted by their dependency level.
- Surgical Execution: Only the selectors for affected nodes are executed.
Irrelevant Update Elimination
If you have a selector s => s.user.name and the count key is updated, SoulState's engine skips this selector entirely. It is never even called, as the engine knows it has no dependency on count.
Computed: Cached Selectors
store.computed allows you to define derived state that is part of the core reactive graph. Unlike component-level selectors, computed values are:
- Cached: Recomputed only when their dependencies change.
- Lazy: Computed only when accessed or when a downstream subscriber requires them.
- Topologically Stable: Guaranteed to run in the correct order in multi-level dependency chains.
const total = store.computed(s => s.price * s.quantity);
// First access: computes and caches
console.log(total.value);
// Second access: returns cached value
console.log(total.value);
Selector Tradeoffs
While automatic tracking is powerful, it has specific characteristics:
- Key-Level Granularity: SoulState tracks dependencies at the top-level key of the store object.
- First-Run Overhead: The first execution of a selector involves proxy overhead to build the initial dependency map.
- Dynamic Re-tracking: If your selector logic changes (e.g.,
s => s.toggle ? s.a : s.b), SoulState automatically updates the dependency graph on the next execution.
Zero Manual Optimization
Unlike Redux or Zustand, you don't need to manually optimize selectors or worry about unstable function references. SoulState's runtime handles the complexity of dependency management for you.