Skip to main content

Selectors

Selectors are the cornerstone of SoulState's design and performance. A selector is a pure function that receives the entire state and returns a specific slice of it.

SoulState is selector‑driven. Components subscribe to state changes through selectors, ensuring precise and efficient updates.

import { useStore } from 'soulstate/react';
import { counterStore } from './store';

// A simple selector extracting `count` from the full state
const countSelector = (state) => state.count;

function Counter() {
const count = useStore(counterStore, countSelector);
return <div>Count: {count}</div>;
}

Why Selectors Matter

Without selectors, a component would subscribe to the entire store, causing re-renders whenever any part of the state updates. Selectors allow focused subscriptions: a component re-renders only when the result of the selector changes.

Golden Rule: Select only the minimal state your component needs.


Selecting Multiple Values

❌ Anti-pattern: Returning a New Object

// Anti-pattern: new object every run → always re-renders
const data = useStore(counterStore, (state) => ({
count: state.count,
user: state.user,
}));

Because the selector returns a new object reference, SoulState's default equality check marks it as "changed" even if values stay identical.

✅ Solution: Use shallow

Use SoulState’s shallow comparison for multi-field selections.

import { shallow } from 'soulstate/utils';

const data = useStore(
counterStore,
(state) => ({
count: state.count,
user: state.user,
}),
shallow
);

Multiple Independent Selectors

For finer granularity and better subscription isolation, call useStore multiple times.

function UserProfile() {
const count = useStore(counterStore, (s) => s.count);
const user = useStore(counterStore, (s) => s.user);
const isActive = useStore(counterStore, (s) => s.isActive);

return (
<div>
<h1>{user.name}</h1>
<p>Count: {count}</p>
<p>Status: {isActive ? 'Active' : 'Inactive'}</p>
</div>
);
}

SoulState batches store notifications, so multiple selector calls do not produce extra emission overhead.


Derived State via Selectors

Selectors can compute derived data based solely on state.

function TodoStats() {
const total = useStore(todoStore, (s) => s.todos.length);
const completed = useStore(todoStore, (s) => s.todos.filter(t => t.completed).length);
const active = useStore(todoStore, (s) => s.todos.filter(t => !t.completed).length);

return (
<div>
<p>Total: {total}</p>
<p>Completed: {completed}</p>
<p>Active: {active}</p>
</div>
);
}

Selectors run whenever the store updates, so expensive selectors should be memoized.


Memoizing Expensive Selectors

SoulState does not memoize selector results internally. If your selector performs heavy work, memoize it using React or libraries like reselect.

Using useMemo

function ExpensiveList() {
const todos = useStore(todoStore, (s) => s.todos);
const filter = useStore(todoStore, (s) => s.filter);

const filtered = useMemo(() => {
return todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
}, [todos, filter]);

return <div>{filtered.length} items</div>;
}

Using reselect

import { createSelector } from 'reselect';

const selectTodos = (s) => s.todos;
const selectFilter = (s) => s.filter;

export const selectVisibleTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
return todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
}
);

// const visible = useStore(todoStore, selectVisibleTodos);

Selector Best Practices

✅ Do

  • Select the smallest possible slice of state
  • Use multiple useStore calls for independent values
  • Use shallow when returning objects
  • Memoize expensive computations
  • Keep selectors pure (depend only on state)

❌ Don’t

  • Return new objects without shallow comparison
  • Select the entire state
  • Insert side effects in selectors
  • Base selector logic on external variables

Key Takeaway

Selectors give you surgical precision over component re-renders. When used correctly, they ensure optimal rendering performance and predictable state‑driven UI behavior in SoulState.