Skip to main content

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:

  1. State Update: setState({ count: 1 }) is called.
  2. Invalidation: The InvalidationGraph identifies all nodes depending on count.
  3. Topological Sort: Affected nodes are sorted by their dependency level.
  4. 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.