Reactive sorting primitives — comparators, reactive sort, sorted search/insert, and granular per-item rank tracking.
| Stage | Category | Version | Last Updated | Demo |
|---|---|---|---|---|
| 0 | Reactivity | 1.0.0-next.0 (next) | Aug 12, 2026 | Demo → |
npm i @solid-primitives/sortable@nextReactive sorting primitives — comparators, reactive sort (with an in-place dirty mode), sorted
search/insert, and two flavors of granular per-item rank tracking built directly on mapArray and
createProjection.
ascending/descending— comparators wherenull/undefined/NaNalways sort to the end, regardless of direction.by— builds a comparator from a derived key.combine— composes comparators, later ones breaking ties left by earlier ones.reverse— flips any comparator.makeSorted— non-reactive sorted copy.createSorted— reactive sort, with a reactive comparator and an in-placedirtymode.sortedIndex/sortedIndexBy— binary search for the insertion point in a sorted array.insertSorted— immutable O(log n) insert into an already-sorted array.createSortedIndex— per-item reactive rank; an item's accessor only updates when that item's position actually changes, built onmapArray(the same primitive behind<For>).createSortedProjection— a store-shaped sorted view, reconciled by key viacreateProjection, so unrelated rows don't notify when one row changes.
ascending / descending
Default comparators for Array.prototype.sort and every primitive in this package.
null/undefined/NaN always sort to the end — unlike a naive a < b ? -1 : a > b ? 1 : 0,
which silently treats them as equal to everything they're compared against.
// Typetype Comparator<T> = (a: T, b: T) => number;const ascending: Comparator<any>;const descending: Comparator<any>;
// Exampleimport { ascending, descending } from "@solid-primitives/sortable";
[3, 1, 2].sort(ascending); // [1, 2, 3][3, 1, 2].sort(descending); // [3, 2, 1][2, undefined, 1].sort(ascending); // [1, 2, undefined]by
Builds a comparator that orders items by a derived key.
// Typefunction by<T, K>(accessor: (item: T) => K, comparator?: Comparator<K>): Comparator<T>;
// Exampleimport { by, descending } from "@solid-primitives/sortable";
const byPrice = by((item: Product) => item.price);products.sort(byPrice);
const byPriceDesc = by((item: Product) => item.price, descending);combine
Composes comparators so later ones break ties left by earlier ones.
// Typefunction combine<T>(...comparators: Comparator<T>[]): Comparator<T>;
// Exampleimport { combine, by, descending } from "@solid-primitives/sortable";
const cmp = combine(by((p: Product) => p.category), by((p: Product) => p.price, descending));products.sort(cmp);reverse
Flips the order of any comparator — composes with by and combine.
// Typefunction reverse<T>(comparator: Comparator<T>): Comparator<T>;
// Exampleimport { reverse, ascending } from "@solid-primitives/sortable";
[1, 3, 2].sort(reverse(ascending)); // [3, 2, 1]makeSorted
Non-reactive: returns a new sorted copy of a plain array, leaving the original untouched.
// Typefunction makeSorted<T>(list: T[], comparator?: Comparator<T>): T[];
// Exampleimport { makeSorted } from "@solid-primitives/sortable";
makeSorted([3, 1, 2]); // [1, 2, 3]createSorted
Reactively sorts list, re-sorting whenever list or comparator changes. comparator may
itself be reactive (an accessor), so toggling sort direction/column doesn't require rebuilding the
primitive.
// Typetype CreateSortedOptions = { /** * Sort the array in place and return that same reference, instead of allocating a new sorted * copy on every recompute. Only meaningful when `list` is a mutable array you own (e.g. paired * with a signal set via `setList(list => (list.sort(cmp), list))`). * @default false */ dirty?: boolean;};
function createSorted<T>( list: MaybeAccessor<T[]>, comparator?: MaybeAccessor<Comparator<T>>, options?: CreateSortedOptions,): Accessor<T[]>;
// Exampleimport { createSorted, by } from "@solid-primitives/sortable";
const [column, setColumn] = createSignal<"name" | "price">("name");const comparator = createMemo(() => by((item: Product) => item[column()]));const sorted = createSorted(products, comparator);
<For each={sorted()}>{product => <Row product={product} />}</For>;Notes
- The default (
dirty: false) path with a static comparator delegates directly to@solid-primitives/signal-builders'ssort()— no reason to re-implement acreateMemowrapper that already exists and is tested. dirty: truemutates and reuses the same array reference — mirrors VueUse'suseSorted'sdirtyoption under Solid's signal model. Only use it on an array you own and don't share.
sortedIndex / sortedIndexBy
Binary search for the leftmost index at which a value could be inserted into an already-sorted array while keeping it sorted.
// Typefunction sortedIndex<T>(list: readonly T[], value: T, comparator?: Comparator<T>): number;function sortedIndexBy<T, K>( list: readonly T[], value: T, accessor: (item: T) => K, comparator?: Comparator<K>,): number;
// Exampleimport { sortedIndex } from "@solid-primitives/sortable";
sortedIndex([1, 3, 5, 7], 4); // 2insertSorted
Immutably inserts a value into an already-sorted array at the position the comparator dictates — an O(log n) search plus an O(n) copy, versus an O(n log n) full re-sort for a single insertion.
// Typefunction insertSorted<T>(list: readonly T[], value: T, comparator?: Comparator<T>): T[];
// Exampleimport { insertSorted } from "@solid-primitives/sortable";
insertSorted([1, 3, 5], 4); // [1, 3, 4, 5]createSortedIndex
Tracks each item's index within the sorted view of list, without invalidating every consumer on
every reorder. Returns a function that, given an item, returns a reactive accessor for its current
index (-1 if the item isn't present).
// Typefunction createSortedIndex<T>( list: MaybeAccessor<T[]>, comparator?: MaybeAccessor<Comparator<T>>,): (item: T) => Accessor<number>;
// Exampleimport { createSortedIndex, by } from "@solid-primitives/sortable";
const indexOf = createSortedIndex(rows, by((r: Row) => r.name));const rank = indexOf(row); // only updates when `row`'s own position changes
<For each={sortedRows()}>{row => <Row row={row} rank={indexOf(row)()} />}</For>;Notes
- Built on
mapArray— the same primitive that powers<For>.mapArraykeys by reference identity and reuses the same computation (and itsindexsignal) for an item across reorders, so an item's index accessor only updates when that item's position actually changes; unrelated moves elsewhere in the list never notify it. This is Solid core's own keyed-diff algorithm, not a bespoke one. - Each call to the returned function does an O(n) scan (untracked) to find the item's stable per-item accessor — cheap relative to the granular reactivity it buys you.
createSortedProjection
A store-shaped sorted view of list, reconciled by key so surviving items keep their store
identity across recomputes — reordering only touches the slots that actually changed, instead of
treating a reorder as "everything changed".
// Typefunction createSortedProjection<T extends object>( list: MaybeAccessor<T[]>, comparator?: MaybeAccessor<Comparator<T>>, key?: string | ((item: T) => unknown),): Refreshable<Store<T[]>>;
// Exampleimport { createSortedProjection, by } from "@solid-primitives/sortable";
const sortedUsers = createSortedProjection(users, by((u: User) => u.name), "id");
<For each={sortedUsers} keyed={u => u.id}> {user => <Row user={user} />}</For>;Notes
keyidentifies an item across recomputes — same contract asreconcile'skeyargument. Defaults to"id".- Prefer this over
createSortedwhen consumers read individual item fields through the store proxy, or render with<For each={projection} keyed={item => item[key]}>— both get move-not-recreate behavior for free from the store's own reconciliation, with no bespoke tracking code in this package at all.
Changelog
See CHANGELOG.md