Comparing main...alpha · TanStack/store · GitHub
Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: TanStack/store
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main
Choose a base ref
...
head repository: TanStack/store
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: alpha
Choose a head ref
Checking mergeability… Don’t worry, you can still create the pull request.
  • 7 commits
  • 52 files changed
  • 6 contributors

Commits on Oct 9, 2026

  1. chore: fix language in CI

    crutchcorn committed Oct 9, 2026
    Configuration menu
    Copy the full SHA
    83630cd View commit details
    Browse the repository at this point in the history
  2. Configuration menu
    Copy the full SHA
    6904b2d View commit details
    Browse the repository at this point in the history
  3. feat(react-store)!: build useSelector on React's useSyncExternalStore…

    … with one selection ref, require React 18+ (#362)
    
    * 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>
    
    * ci: apply automated fixes and generate docs
    
    * feat(react-store)!: require React 18+, use the built-in useSyncExternalStore
    
    Review feedback on #362 asked to change the supported React versions
    rather than keep the `use-sync-external-store` shim around for React 16.8
    and 17. The peer range is now `react` / `react-dom` `^18.0.0 || ^19.0.0`,
    so `useSelector` imports `useSyncExternalStore` from `react` and the
    `use-sync-external-store` dependency and its types are removed. The
    consumer bundle (react-store, minified, `react` and `@tanstack/store`
    external) goes from 3385 B raw / 1510 B gzip with both shim modules to
    1347 B raw / 646 B gzip.
    
    Because dropping React 16/17 is breaking, the changeset is now `major`
    and the repo enters changesets pre mode with the `alpha` tag
    (`.changeset/pre.json`), so the release lands as
    `@tanstack/react-store@1.0.0-alpha.0`. `docs/installation.md` states the
    new minimum React version.
    
    Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
    
    * ci: apply automated fixes and generate docs
    
    * fix(react-store): call unsubscribe on the subscription object
    
    `useSelector` handed React the `unsubscribe` method detached from the
    subscription object, both before this PR (destructured) and in the
    rewrite. `SelectionSource` is structural, so a source whose `unsubscribe`
    relies on `this` (a class-based subscription, for example) satisfies the
    type but threw `TypeError` from React's effect cleanup and stayed
    subscribed. The cleanup is now a closure that calls
    `subscription.unsubscribe()`.
    
    Adds a regression test with a class-based subscription that fails with
    "Cannot set properties of undefined (setting 'closed')" on the previous
    code, and tidies the ReactDOM sentence in docs/installation.md.
    
    Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
    
    * perf(react-store): keep useSelector callbacks in one ref, stable across renders
    
    `useSelector` still paid for three of its own hook slots per render (two
    `useCallback`s plus the store hook; React clones every hook object on each
    re-render and `useCallback` allocates the closure and deps array every
    time) and handed `useSyncExternalStore` a fresh `getSnapshot` each render.
    React compares `getSnapshot` by identity: whenever it changes it flags the
    fiber for passive effects, pushes an `updateStoreInstance` effect (plus a
    `bind`) and, in transitions, a store consistency check that calls
    `getSnapshot` again, even when nothing about the component changed.
    
    The hook now keeps a single instance in one `useRef`: the `subscribe` and
    `getSnapshot` callbacks together with the `source`, `selector` and
    `compare` they were built for. A new instance is only created when one of
    those inputs changes; `subscribe` is carried over unless the source
    changed, so React re-subscribes only then. Both closures capture their
    inputs instead of reading them from the ref, so a render that suspends
    with a different selector cannot change what the committed subscription
    selects (the suspended-transition test still passes). The selection
    record is shared by all instances of a component so inline selectors
    keep their identity-stable results, and it is now keyed on the compare
    function as well: after a compare-equal update the record advances its
    snapshot while keeping the previous selection, and a later render with a
    different `compare` used to hit that memo without ever consulting the new
    function. The with-selector shim keyed its memo on `isEqual`, so this
    restores parity; a new test pins it and fails on the previous commit.
    
    Measured with a throwaway vitest bench (production React 19.2.5, jsdom,
    200 subscribed components, mean per operation): parent re-render with
    stable selectors and an unchanged store 0.164 -> 0.128 ms (-22%), inline
    selectors 0.161 -> 0.153 ms (-5%), store update re-rendering all 200
    0.203 -> 0.188 ms (-8%), mount + unmount 0.795 -> 0.732 ms (-8%). A
    variant that kept `useCallback` for both callbacks was slower than the
    previous code, so the extra hook slot costs more than the skipped effect
    saves. The consumer bundle (react-store minified, react and
    @tanstack/store external) is 1347 -> 1660 B raw, 646 -> 740 B gzip for
    this, still down from 3385 / 1510 B on main.
    
    Tests: source switch moves the subscription and reads the new source; the
    compare function from the latest render is used. Docs: the installation
    page no longer claims ReactDOM-only support, React Native works as well.
    
    Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
    
    * ci: apply automated fixes and generate docs
    
    * perf(react-store): key the useSelector memo on the getSnapshot closure
    
    Flatten useSelector's per-component state into one object that is
    mutated in place instead of an instance that was re-created on every
    input change plus a nested selection record, and key the memoized
    selection on the `getSnapshot` closure that computed it. That closure
    already captures the source, selector and compare it was built for, so
    the memo hit is two identity checks (owner, snapshot) instead of four,
    the record needs no selector/compare fields, and the three factory
    functions and the `previous` plumbing go away.
    
    Per render this removes one object allocation for inline selectors (only
    the closure is created now) and the nested record indirection from every
    `getSnapshot` call; a mount allocates one object instead of two. The
    stable path is unchanged: one ref, three comparisons, no allocations, no
    effects. A whole-render bench with 200 components cannot separate this
    from the previous commit (the hook is now a small fraction of React's
    per-component work), so the gain is by operation count.
    
    useSelector minified: 812 -> 595 B raw, 400 -> 343 B gzip. Consumer
    bundle (react-store minified, react and @tanstack/store external):
    1660 -> 1443 B raw, 740 -> 676 B gzip; main ships 3385 / 1510 B.
    
    Dropping the owner check makes the suspended-transition, selector-switch
    and compare-change tests fail, so the key stays pinned.
    
    Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
    
    * ci: apply automated fixes and generate docs
    
    * ci: apply automated fixes and generate docs
    
    * chore: update changsets file
    
    ---------
    
    Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
    Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
    Co-authored-by: Corbin Crutchley <git@crutchcorn.dev>
    4 people authored Oct 9, 2026
    Configuration menu
    Copy the full SHA
    5567af4 View commit details
    Browse the repository at this point in the history
  4. [alpha] Drop Vue 2 Support (#389)

    chore: remove Vue 2 support
    crutchcorn authored Oct 9, 2026
    Configuration menu
    Copy the full SHA
    c9bef08 View commit details
    Browse the repository at this point in the history

Commits on Oct 10, 2026

  1. ci: Version Packages (alpha) (#388)

    Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
    github-actions[bot] authored Oct 10, 2026
    Configuration menu
    Copy the full SHA
    4ec9788 View commit details
    Browse the repository at this point in the history
  2. fix(react-store): preserve RSC client boundaries (#391)

    * test: cover React Store RSC boundaries and ordinary SSR
    
    * fix(react-store): preserve React Server Component client boundaries
    schiller-manuel authored Oct 10, 2026
    Configuration menu
    Copy the full SHA
    9559024 View commit details
    Browse the repository at this point in the history

Commits on Oct 21, 2026

  1. Configuration menu
    Copy the full SHA
    6d2b3bd View commit details
    Browse the repository at this point in the history
Loading