ilokesto

Selectors

Efficiently subscribing to state slices in @ilokesto/state.

Selectors

Selectors are the primary mechanism for reading state in @ilokesto/state. They allow components to subscribe to specific data slices, narrowing the surface of re-renders.

It is important to understand that selectors narrow reads, not writes. Even if you select a single primitive field, the "writer" (the second element in the returned tuple) still expects and operates on the full store state.

Basic Usage

The hook returned by create accepts a selector function as its first argument.

const [count, setCount] = useCounter(state => state.count);

// Even though we only read 'count', setCount still receives full state
const increment = () => setCount(prev => ({ ...prev, count: prev.count + 1 }));

The hook returns a tuple:

  1. Selected value: The result of your selector function.
  2. Writer: The setState or dispatch function for the entire store.

Memoization and Comparison

@ilokesto/state uses a "runtime-first" memoization model tied to the store's internal snapshot cycle. It does not provide a public equality API (like shallow or isEqual parameters) because it relies on the underlying store's structural sharing.

  • The comparison logic: When the store updates, the hook compares the previous store snapshot with the next store snapshot using a deep equality check (deepCompare).
  • Reuse behavior: If the internal snapshots are deeply equal, the previous selected value is reused exactly. This ensures that even if you return an object or array from a selector, it won't trigger a re-render unless the data inside actually changed.

The "No-Selector" Path

If you call the hook without a selector, you are opting into a whole-store subscription.

// Subscribes to EVERYTHING. The component re-renders on every single store update.
const [state, setState] = useStore();

While convenient for small stores or top-level providers, this should be avoided in performance-critical components.

Selector Identity and Resets

The hook tracks the identity of your selector function. If the selector function itself changes (e.g., it's an inline function that captures different props), the hook may reset its internal subscription state.

To ensure the best performance:

  • Keep selectors stable (define them outside the component if they don't depend on props).
  • If a selector must depend on props, ensure those props are stable or accept that the selector will be re-evaluated.

SSR and Initial State

When using Server-Side Rendering (SSR), selectors interact with getInitialState(). The hook uses the initial state provided during store creation (or injected via SSR hydration) to generate the first snapshot. This ensures that the first render on the client matches the server output even before the first client-side effect runs.

Derived State

You can compute derived values directly within a selector.

const [isOverLimit] = useCounter(state => state.count > 100);

Since the hook only triggers a re-render if the result of the selector changes, isOverLimit will only cause a re-render when the count crosses the threshold, not for every count increment.

Common Mistakes

  1. Expecting narrow writes: Thinking setCount(5) will work if you selected state.count. Writers always deal with the full store shape defined in create.
  2. Expensive logic in selectors: The selector path is optimized, but it is not free. When store snapshots differ, the selector is evaluated again. Keep selectors cheap and predictable.
  3. Circular dependencies: Defining a selector that depends on a value it also writes to in a way that triggers infinite loops.

On this page