[]previous and next page

Selection in constant time

A highlighted row is the classic list update that scales badly: class={{ selected: selected() === row().id }} makes every row read selected(), so one click re-runs…

For agentsUpdated

A highlighted row is the classic list update that scales badly: class={{ selected: selected() === row().id }} makes every row read selected(), so one click re-runs one binding per row. useSelector(source, equals?) returns isSelected(key). A binding that calls isSelected(id) subscribes only to that key's boolean. When the source changes from a to b, only the bindings for a and b re-run.

src/table.tsxtsx
import { useSelector, useSignal, useStore } from "@shadow-js/core";
import { For } from "@shadow-js/web";

export const reads = { selector: 0, naive: 0 };

interface Row {
  id: number;
  label: string;
}

export function Table(props: { size?: number }) {
  const size = props.size ?? 3;
  // useSignal: replaced as a whole on purpose (a store would reconcile it).
  const [rows] = useSignal<Row[]>(Array.from({ length: size }, (_, id) => ({ id, label: `row ${id}` })));
  const [selected, setSelected] = useStore<number | null>(null);
  const isSelected = useSelector(selected);

  return (
    <div>
      <For each={rows()} key={(row) => row.id}>
        {(row) => (
          <p
            class={{ picked: (reads.selector += 1, isSelected(row().id)) }}
            data-naive={(reads.naive += 1, selected() === row().id)}
            onClick={() => setSelected(row().id)}
          >
            {row().label}
          </p>
        )}
      </For>
    </div>
  );
}

Details of useSelector:

  • Create the selector before the rows that consume it, so each consumer runs once per change.
  • It tracks source with a render-phase effect in the current owner. Called outside a tracked context, isSelected(key) reads the source directly and creates no subscription.
  • The default comparison is === (so NaN never matches). Pass equals(key, value) for other relations, such as "row id is in the selected set": that costs one equals call per registered key per change instead of two lookups.