Skip to content
usefy

useSelection

State & data

Multi/single selection state for lists and tables — Set-based, checkbox-ready

Install

$ npm install @usefy/use-selection

Quick start

useSelection.tsx
import { useSelection } from "@usefy/use-selection";

type User = { id: number; name: string };

function UserTable({ users }: { users: User[] }) {
  const {
    selected,
    isSelected,
    toggle,
    selectAll,
    clear,
    isAllSelected,
    isPartiallySelected,
  } = useSelection(users, { getKey: (u) => u.id });

  return (
    <table>
      <thead>
        <tr>
          <th>
            <input
              type="checkbox"
              checked={isAllSelected}
              ref={(el) => {
                if (el) el.indeterminate = isPartiallySelected;
              }}
              onChange={() => (isAllSelected ? clear() : selectAll())}
            />
          </th>
          <th>Name ({selected.length} selected)</th>
        </tr>
      </thead>
      <tbody>
        {users.map((user) => (
          <tr key={user.id}>
            <td>
              <input
                type="checkbox"
                checked={isSelected(user)}
                onChange={() => toggle(user)}
              />
            </td>
            <td>{user.name}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

API reference

const result = useSelection<T>(items: T[], options?: UseSelectionOptions<T>);

Options — UseSelectionOptions<T>

OptionTypeDefaultDescription
getKey(item: T) => string | numberidentity ((item) => item)Derives the selection key for an item. The Set stores these keys, not items, so selection survives new object references. Provide this for object items (e.g. (row) => row.id); the identity default is only correct for primitive items.
multiplebooleantruetrue = multi-selection; false = single-selection (selecting replaces the current choice, and selectAll is a no-op).

Return — UseSelectionReturn<T>

PropertyTypeDescription
selectedT[]The selected items, in items order. Derived as items ∩ selectedKeys — items removed from items drop out automatically.
selectedKeysReadonlySet<string | number>The raw set of selected keys (source of truth). May contain keys for items no longer in items; those are invisible in the derived values.
isSelected(item: T) => booleanWhether an item is selected (by key). Stable.
toggle(item: T) => voidFlip an item's selection. In single mode, selecting a new item replaces the previous one. Stable.
select(item: T) => voidSelect an item (idempotent). In single mode, replaces the selection. Stable.
deselect(item: T) => voidDeselect an item (idempotent). Stable.
selectAll() => voidSelect every item in items. No-op when all are already selected, or in single mode. Stable.
clear() => voidClear the whole selection (deselect all). No-op when already empty. Stable.
isAllSelectedbooleantrue when every item is selected. Always false for an empty items array.
isPartiallySelectedbooleantrue when some — but not all — items are selected (drives an indeterminate header checkbox). false for empty items.
isNoneSelectedbooleantrue when no items are selected (including when items is empty).

Behavior notes

  • Selection key. The Set stores keys, never items. This is what makes a selection stable when items is rebuilt with fresh object references each render — as long as getKey maps the same logical item to the same key. For primitive items (string/number) the identity default just works.
  • Items-change reconciliation. selectedKeys is the source of truth; every item-facing value is derived from the current items. Removing a selected row from items makes it disappear from selected and recomputes isAllSelected/isPartiallySelected automatically — no manual pruning needed. The removed key stays in selectedKeys (harmless and invisible) so it re-appears if the item comes back.
  • Empty list. isAllSelected is false for an empty items array (there is nothing to have "all" selected); isNoneSelected is true.
  • Single-selection mode. With multiple: false, select/toggle of a new item replace the current selection; toggling the already-selected item deselects it. selectAll is a no-op (you cannot hold more than one selection).
  • StrictMode / concurrency. Updates are immutable (a fresh Set on every change) and no user callback runs inside a setState updater, so it is safe under StrictMode and concurrent rendering.

Go deeper

This page is the quick reference. For every example, prop, and edge case, read the full README — or open Storybook to change props live.

More in state & data