ta
TanStack
GitHub
store
Workspace
GitHub
CI Pipeline Executions
Current execution
Succeeded
schiller-manuel-react-store-useselector-single-ref
Runs and Agents
Configuration
Self-Healing CI
Project Graph
Circular Dependencies
Enterprise
Resource Usage
Analysis
Conformance
Enterprise
Runs
Current run
Succeeded
Tasks
Resource Usage
Insights
Compare tasks
Analytics
Sign in
Toggle sidebar
perf(react-store): build useSelector on useSyncExternalStore with one selection ref `useSelector` wrapped `use-sync-external-store/shim/with-selector`. Per subscribed component and per render that stack ran two `useCallback`s in `useSelector` (`subscribe`, `getSnapshot`) and, inside the shim, a `useRef`, a `useMemo` with four deps that rebuilt the memoized selector whenever the (usually inline) selector changed identity, a `useEffect` copying the committed value into the ref, `useDebugValue`, and finally `useSyncExternalStore`: about seven hook slots and six allocations per render plus a passive effect React had to traverse on every commit. Measured in TanStack Router with 200 mounted `<Link>`s, a plain `useSyncExternalStore` plus a single ref cut retained heap by 8% (2738 -> 2512 KB) and re-render CPU by about 5% on renders that recompute the selection. `useSelector` now calls `useSyncExternalStore` from `use-sync-external-store/shim` directly. One `useRef` holds the last `{ selector, snapshot, selected }` record, mutated in place. `getSnapshot` reads `source.get()`; when the record's selector and snapshot are identical (`===`) it returns the stored selection, otherwise it runs the selector and, when `compare(previous, next)` holds, keeps the previous selection so `useSyncExternalStore` sees an unchanged value and skips the re-render. Keying the memo on the selector identity as well as the snapshot is what keeps a render that suspends with a different selector (pinned by the existing suspended-transition test) from poisoning the committed selector's selection. As in the with-selector shim, `compare` runs against the previous selection regardless of which selector produced it, which is what keeps inline selectors identity-stable across re-renders. The default identity selector is hoisted so `useSelector(atom)` hits the memo too. `subscribe` stays memoized on `[source]`: React re-subscribes in a passive effect whenever `subscribe` changes identity (its deps array is `[subscribe]`), so a per-render closure would tear down and recreate the store subscription on every render. `getSnapshot` is a plain closure: it has to read this render's `selector` and `compare`, which are usually inline and would defeat a `useCallback` anyway; React only compares its identity to decide whether to re-check the store after commit. The base shim is kept because the peer range still includes React 16.8 and 17, which have no native `useSyncExternalStore`; on React 18+ the shim delegates to the native hook. Only the `with-selector` entry is dropped, so that module leaves consumer bundles (react-store + shim, minified: 3385 -> 2808 B raw, 1510 -> 1331 B gzip). Public API and semantics are unchanged; all existing tests pass unmodified. New tests pin that a stable selector is not re-run on a re-render with an unchanged store value, that `compare` returning true keeps the previous selection identity without re-rendering, that a new selector is re-run and its selection returned, and that a store update re-runs the installed selector exactly once. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
nx affected --targets=test:sherif,test:knip,test:eslint,test:lib,test:types,test:build,build --parallel=3
⌘K
Succeeded
nx affected --targets=test:sherif,test:knip,test:eslint,test:lib,test:types,test:build,build --parallel=3
Click to copy
read-write
access token used
fdcad7b2
362
17%
Cache hits
3 of 18 tasks used cache.
Atomizer enabled
8 groups of tasks optimized.
GitHub
store
Workspace