createSelector
createSelector creates a reactive selector function optimized for the
“one-of-many” selection pattern common in lists. Instead of every row observing
the selection signal (O(n) reactions per change), only the previously-selected
and newly-selected items are notified. With the default equality comparer,
selection changes stay O(1).
Signature
function createSelector<T>(
source: () => T,
equals?: (a: T, b: T) => boolean,
): Selector<T>
type Selector<T> = ((key: T) => boolean) & {
dispose(): void
getAdmin(key: T): ObservableAdmin<boolean>
}
Parameters
| Parameter | Type | Description |
|---|---|---|
source |
() => T |
A reactive function returning the current selected key |
equals |
(a, b) => boolean |
Optional custom equality (defaults to identity with NaN handling) |
Returns a Selector<T> function: call it with a key to get a reactive
boolean.
The problem
With a naive approach, every row in a list observes the selection observable. When the selection changes, all N rows re-evaluate:
// ❌ Naive approach — O(n) re-evaluations per selection change
const selectedId = observableBox("item-1")
function Row({ id }) {
autorun(() => {
const isActive = selectedId.get() === id // ALL rows track selectedId
setStyle(isActive ? "active" : "inactive")
})
}
The solution
createSelector uses per-key tracking so only the old and new selection
re-evaluate when you use the default equality comparer:
// ✅ Efficient — O(1) re-evaluations per selection change with the default comparer
import { autorun, createSelector, observableBox } from "@fobx/core"
const selectedId = observableBox("item-1")
const isSelected = createSelector(() => selectedId.get())
function Row({ id }) {
autorun(() => {
const isActive = isSelected(id) // only THIS key is tracked
setStyle(isActive ? "active" : "inactive")
})
}
// When selectedId changes from "item-1" to "item-2":
// - Only the Row with id="item-1" re-runs (now false)
// - Only the Row with id="item-2" re-runs (now true)
// - All other rows: no work
How it works
- An internal
autoruntracks thesourcefunction. - When the source value changes, the selector notifies only two per-key admins:
the one for the previous value (now
false) and the one for the new value (nowtrue). - Each key’s admin is lazily created on first access and cleaned up when it loses all observers.
Outside tracked context
When called outside a reaction (no active tracking), isSelected(key) returns a
plain boolean without creating subscriptions:
console.log(isSelected("item-1")) // true (plain boolean, no tracking)
Custom equality
const isSelected = createSelector(
() => selectedCoords.get(),
(a, b) => a.x === b.x && a.y === b.y,
)
When you supply a custom equals function, createSelector still keeps
re-renders targeted to the affected keys, but source changes scan subscribed
keys to find matches.
Dispose
Call dispose() to tear down the internal autorun and all per-key admins:
isSelected.dispose()