{{ message }}
Repository navigation
Permalink
Choose a base ref
{{ refName }}
default
Choose a head ref
{{ refName }}
default
Checking mergeability…
Don’t worry, you can still create the pull request.
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
Could not load branches
Nothing to show
Loading
Could not load tags
Nothing to show
{{ refName }}
default
Loading
...
head repository: TanStack/store
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: changeset-release/alpha
Could not load branches
Nothing to show
Loading
Could not load tags
Nothing to show
{{ refName }}
default
Loading
- 8 commits
- 52 files changed
- 6 contributors
Commits on Oct 9, 2026
-
Configuration menu - View commit details
-
Copy full SHA for 83630cd - Browse repository at this point
Copy the full SHA 83630cdView commit details -
Configuration menu - View commit details
-
Copy full SHA for 6904b2d - Browse repository at this point
Copy the full SHA 6904b2dView commit details -
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>
Configuration menu - View commit details
-
Copy full SHA for 5567af4 - Browse repository at this point
Copy the full SHA 5567af4View commit details -
Configuration menu - View commit details
-
Copy full SHA for c9bef08 - Browse repository at this point
Copy the full SHA c9bef08View commit details
Commits on Oct 10, 2026
-
ci: Version Packages (alpha) (#388)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Configuration menu - View commit details
-
Copy full SHA for 4ec9788 - Browse repository at this point
Copy the full SHA 4ec9788View commit details -
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
Configuration menu - View commit details
-
Copy full SHA for 9559024 - Browse repository at this point
Copy the full SHA 9559024View commit details
Commits on Oct 21, 2026
-
Configuration menu - View commit details
-
Copy full SHA for 6d2b3bd - Browse repository at this point
Copy the full SHA 6d2b3bdView commit details -
Configuration menu - View commit details
-
Copy full SHA for 7f09af3 - Browse repository at this point
Copy the full SHA 7f09af3View commit details
Loading
This comparison is taking too long to generate.
Unfortunately it looks like we can’t render this comparison for you right now. It might be too big, or there might be something weird with your repository.
You can try running this command locally to see the comparison on your machine:
git diff main...changeset-release/alpha
You can’t perform that action at this time.
