React utilities for keyed selection state. It is useful for large selectable lists where changing the selected item should update only the previously selected row, the newly selected row, and any selected-value display.
The API is similar in spirit to Solid's createSelector: each row subscribes to
whether its own key is selected instead of receiving the selected key as a prop
from the parent list.
showcase.mp4
npm install @leesf/use-selectionyarn add @leesf/use-selectionpnpm add @leesf/use-selectionbun add @leesf/use-selectionReact is a peer dependency.
Use SelectionProvider with SelectedKey and IsSelectedKey when you do not
want to pass the store through every row. This is the recommended path for list
UIs because selected state stays local to the components that actually need it.
import {
IsSelectedKey,
SelectionProvider,
SelectedKey,
useSelectionStore,
} from "@leesf/use-selection";
const items = [
{ id: "one", label: "One" },
{ id: "two", label: "Two" },
{ id: "three", label: "Three" },
];
export function List() {
return (
<SelectionProvider initialKey={items[0]!.id}>
<SelectedKey>
{(selectedKey) => (
<p>Selected: {(selectedKey as string | null) ?? "None"}</p>
)}
</SelectedKey>
{items.map((item) => (
<IsSelectedKey key={item.id} keyValue={item.id}>
{(isSelected) => <Row isSelected={isSelected} item={item} />}
</IsSelectedKey>
))}
</SelectionProvider>
);
}
function Row({
isSelected,
item,
}: {
isSelected: boolean;
item: { id: string; label: string };
}) {
const store = useSelectionStore();
return (
<button
aria-pressed={isSelected}
onClick={() => store.setSelectedKey(item.id)}
type="button"
>
{item.label}
</button>
);
}You can also provide a store directly when you want to own it outside the provider:
const store = createSelectionStore("one");
<SelectionProvider store={store}>
<SelectedKey>{(selectedKey) => selectedKey}</SelectedKey>
</SelectionProvider>;Use keyValue with IsSelectedKey because key is a special React prop and is
not passed through to components.
You can pass a store directly to the wrapper components. This avoids context and keeps TypeScript inference from the store.
import {
createSelectionStore,
IsSelectedKey,
SelectedKey,
} from "@leesf/use-selection";
import { useMemo } from "react";
const items = [
{ id: "one", label: "One" },
{ id: "two", label: "Two" },
];
function ListWithoutProvider() {
const store = useMemo(() => createSelectionStore(items[0]!.id), []);
return (
<div>
<SelectedKey store={store}>
{(selectedKey) => <p>Selected: {selectedKey ?? "None"}</p>}
</SelectedKey>
{items.map((item) => (
<IsSelectedKey key={item.id} keyValue={item.id} store={store}>
{(isSelected) => (
<RowWithoutProvider
handleSelect={store.setSelectedKey}
isSelected={isSelected}
item={item}
/>
)}
</IsSelectedKey>
))}
</div>
);
}
function RowWithoutProvider({
handleSelect,
isSelected,
item,
}: {
handleSelect: (id: string) => void;
isSelected: boolean;
item: { id: string; label: string };
}) {
return (
<button
aria-pressed={isSelected}
onClick={() => handleSelect(item.id)}
type="button"
>
{item.label}
</button>
);
}In this example, selectedKey is inferred as string | null because store is
SelectionStore<string>.
The hooks are exported for advanced cases, but the wrapper components are the recommended default. Components make it harder to accidentally pull selected state into the parent list and rerender every row.
import {
createSelectionStore,
useIsSelectedKey,
useSelectedKey,
} from "@leesf/use-selection";
import { useMemo } from "react";
function HookList() {
const store = useMemo(() => createSelectionStore("one"), []);
const selectedKey = useSelectedKey(store);
return (
<div>
<p>Selected: {selectedKey ?? "None"}</p>
<HookRow handleSelect={store.setSelectedKey} id="one" store={store} />
<HookRow handleSelect={store.setSelectedKey} id="two" store={store} />
</div>
);
}
function HookRow({
handleSelect,
id,
store,
}: {
handleSelect: (id: string) => void;
id: string;
store: ReturnType<typeof createSelectionStore<string>>;
}) {
const isSelected = useIsSelectedKey(store, id);
return (
<button
aria-pressed={isSelected}
onClick={() => handleSelect(id)}
type="button"
>
{id}
</button>
);
}Creates a selection store.
const store = createSelectionStore<string>("item-1");The store exposes:
setSelectedKey(next)- sets the selected key.getSelectedKeySnapshot()- returns the current selected key.subscribeSelectedKey(listener)- subscribes to selected-key changes.getIsSelectedKeySnapshot(key)- returns whetherkeyis selected.subscribeIsSelectedKey(key, listener)- subscribes to changes for one key.
Provides a selection store to SelectedKey, IsSelectedKey, and
useSelectionStore.
Create an internal store:
<SelectionProvider initialKey="item-1">{children}</SelectionProvider>Provide an existing store:
const store = createSelectionStore("item-1");
<SelectionProvider store={store}>{children}</SelectionProvider>;initialKey is only used when the provider creates its internal store.
Reads the nearest SelectionProvider.
const store = useSelectionStore();Subscribes to the selected key value.
const selectedKey = useSelectedKey(store);Subscribes to whether a specific key is selected.
const isSelected = useIsSelectedKey(store, item.id);Component wrapper around useSelectedKey.
<SelectedKey store={store}>
{(selectedKey) => <span>{selectedKey}</span>}
</SelectedKey>Component wrapper around useIsSelectedKey.
<IsSelectedKey keyValue={item.id} store={store}>
{(isSelected) => <Row selected={isSelected} />}
</IsSelectedKey>If a parent list stores selectedId in React state and passes it to every row,
each selection change rerenders the parent and gives every row a changed prop.
That is often fine for small lists, but expensive for large or complex rows.
With @leesf/use-selection, rows subscribe by key. Updating the selected key
notifies only:
- the previously selected key
- the newly selected key
- subscribers to the selected key value
Run the playground to compare normal React state selection with keyed subscriptions:
pnpm run playBuild and preview the production playground:
pnpm run play:build
pnpm run play:previewThe playground shows per-row render counts so you can see the difference when selecting items.
pnpm install
pnpm run typecheck
pnpm run test
pnpm run build