Meeshan.dev
Shadcn RegistryHooks

use-selection

A general-purpose hook for managing selection state (rows, cards, lists) without triggering unnecessary re-renders.

Installation

pnpm dlx shadcn add https://meeshan.dev/r/use-selection.json

Problem it solves

If you've ever built a component with selection — whether it's a table, a list of cards, or even a bunch of badges — you know the struggle. Usually, you'd store the selected IDs in a piece of state. But here's the catch: every time you toggle a selection, the entire parent component (and all its children) re-renders.

I've talked about this in detail over at /react-patterns/ui-selection. The core issue is that standard React state is too "loud" — it notifies everyone when something changes. While I often use tables as the primary example because that's where the performance "foot-gun" is most obvious, this problem hits you anywhere you have a large collection of selectable items.

useSelection (the core of this hook) solves this by using an ObservableMap. Instead of the whole list listening to the selection state, individual items can subscribe to just the bits they care about. This means clicking a checkbox or selecting a card only re-renders that specific item. It's a massive performance win, and I am really happy with how it turned out.

Usage

To get this working, you first need an ObservableMap to act as your store. I usually initialize it in a useRef to keep it stable across renders. Then, you can use useSelection to interact with it.

While this works for any selectable UI, here is how I'd set it up for a table, which is where most developers hit those nasty performance walls:

import React from 'react';

import { useSelection } from '@/registry/hooks/use-selection';
import { ScopeProvider, useScopeCtx } from '@/registry/lib/scope-provider';
import {
  observableMap,
  type ObservableMap,
} from '@/registry/lib/observable-map';

type SelectionStore = ObservableMap<number, boolean>;

export function TableExample() {
  // I create the store once and keep it stable
  const selectionStore = React.useRef<SelectionStore>(observableMap()).current;

  return (
    <ScopeProvider value={selectionStore}>
      <table>
        <thead>
          <tr>
            <th>
              <SelectAll data={[1, 2, 3]} />
            </th>
            <th>Name</th>
          </tr>
        </thead>
        <tbody>
          {[1, 2, 3].map((id) => (
            <tr key={id}>
              <td>
                <SelectRow id={id} />
              </td>
              <td>Row {id}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </ScopeProvider>
  );
}

function SelectRow({ id }: { id: number }) {
  const store = useScopeCtx<SelectionStore>();

  const { select, deselect, subscribe, has } = useSelection({ store });

  const isSelected = React.useSyncExternalStore(
    subscribe,
    () => has(id),
    () => false,
  );

  return (
    <input
      type='checkbox'
      checked={isSelected}
      onChange={(e) => {
        if (e.target.checked) {
          select(id, true);
        } else {
          deselect(id);
        }
      }}
    />
  );
}

API Reference

Parameters

PropTypeDescription
storeObservableMap<Key, Value>The observable map instance that holds the selection state.

Returns

PropTypeDescription
getSelectedCount() => numberReturns the number of selected items.
update(key: Key, value: Value) => voidUpdates a key's value if it exists.
select(key: Key, value: Value) => voidAdds a key to the selection store.
deselect(key: Key) => voidDeletes a key from the selection.

for all other props kindly check observable-map

On this page