- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 3.3k
 
[DRAFT] Initial React "concurrent stores" compat prototype #2263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
          ConnectI think leaving them behind is probably fine for now. We can revisit if we get significant pushback. Store EnhancersAgree, next step is to pressure test some enhancers combinations and validate that they behave as expected. SubscriptionsMy implementation was just written to get it working quickly. Would love to update the subscription code to have more sensible perf characteristics. Linked list sounds like a good solution! Would you like to submit a PR? useStoreSelector ChangesUnstable SelectorsI'm working on thejustinwalsh/react-concurrent-store#13 isEqualRather than supporting isEqual I'd like to explore passing the selector the current and previous values. This would allow libraries like Relay to implement  I think this can be used to implement a custom  Error handlingAre the error handling semantics of Redux selectors documented anywhere? Because selectors are eager, there's a zombie child problem where you may evaluate a selector which is not actually expected to be read in the new state, so we may want to swallow those errors, similar to how React swallows errors in updater functions.  | 
    
| 
           I can imagine that passing  The main bit of documentation we've had around "zombie children" and selector errors is here: 
 I probably haven't updated that paragraph since before we switched to  Also a bit of historical context here:  | 
    
| 
           Re selector arguments, I think from React's perspective, there are no selector arguments. They must be pre-bound into the selector, or use a "higher order selector" where the selector returns a partially applied function which accepts the arguments:   | 
    
Implements a proof-of-concept demonstrating React's upcoming "concurrent stores" pattern (also called "store pic") for useLiveQuery. This enables concurrent-safe behavior with React transitions and prevents UI tearing. Background: - React's useSyncExternalStore forces synchronous updates, breaking concurrent features - React is introducing a concurrent stores API to enable external stores to work properly with transitions, Suspense, and concurrent rendering - This POC adapts the pattern from react-concurrent-store and react-redux PR #2263 Key Components: - CollectionStore: Wraps TanStack Collections with committed/pending snapshots - useLiveQueryConcurrent: Alternative to useLiveQuery using the store pic pattern - CollectionStoreProvider: Context provider for managing store commits - StoreManager: Tracks store commits across the React tree with reference counting Features: - Maintains dual snapshots (committed vs pending) for concurrent safety - Implements state rebasing when sync updates occur during transitions - Prevents tearing by ensuring components mounting mid-transition see consistent state - Clever mounting strategy that entangles with ongoing transitions - Reference-counted store management for proper cleanup Documentation: - README.md: Overview and usage guide - COMPARISON.md: Detailed comparison with current useLiveQuery - TECHNICAL.md: Deep dive into implementation internals - example.tsx: Comprehensive usage examples Benefits: - Works properly with React transitions (non-blocking, interruptible) - Prevents UI tearing when components mount during transitions - Aligns with upcoming React concurrent stores API - Better UX through non-blocking updates Trade-offs: - Requires CollectionStoreProvider wrapper - Slightly higher memory usage (dual snapshots) - Small performance overhead for commit tracking - Uses React internals (experimental, subject to change) References: - reduxjs/react-redux#2263 - https://github.com/thejustinwalsh/react-concurrent-store - https://react.dev/blog/2025/04/23/react-labs-view-transitions-activity-and-more#concurrent-stores
This PR:
useSyncExternalStorereactStoreEnhancerfrom the polyfill tests<Provider>andReactReduxContextto render the polyfill's<StoreProvider>component and thereactStoreinstance in contextuseSelectorto call the newuseStoreSelectorhook instead ofuseSyncExternalStore, and adds wrapper logic that tries to backfill support for equality checks and unstable selector references (hopefully to be handled by the hook itself later on)createTestStoreutil that configures the store with the React store enhanceruseSelector.spec.tsxto use thatcreateTestStoreutil for all of its storesuseSelector.spec.tsxto mostly pass:StoreManagerclass to manage subscriptionsStoreManageris using an array and not a linked list like ourSubscriptionclassBackground
@captbaritone and @thejustinwalsh have been working in https://github.com/thejustinwalsh/react-concurrent-store to prototype the upcoming React "concurrent stores" API. As a loose description, what we're hoping for is "
useSyncExternalStore, minus theSync" - aka a concurrent/transition-compatible way to integrate external state into React.They've got a first POC polyfill up on NPM as
react-concurrent-store, and the repo has a couple tiny demo integrations for Relay and Redux.This is my attempt to do an experimental integration and rewrite React-Redux to use it and see how far we get, a la the alpha integration of
useSyncExternalStorein #1808 .Results
Impressively, especially for just a couple hours of hacking:
All but 3
useSelectortests pass!The caveat is that I did change pretty much all places where we assert the number of selector calls (which all went up 2+ calls), and disabled a couple places where we asserted that we had N active internal subscriptions (as this mechanism bypasses our own Subscription implementation).
The test failures are:
Analysis
I believe our own v7
useSelectorimplementation would catch errors, swallow them, and force a re-render (under the assumption that if you had a stale props / "zombie child" scenario, then the problems would all go away by the time the component was done re-rendering). I don't know what the currentuSESsemantics are, but hereuseStoreSelectoris clearly doing something different.The selector stability test is obviously failing due to some combo of the actual
useStoreSelectorimplementation not actually supporting unstable selector references and the attempt to make them work in userland here.Overall, though, this is a pretty solid indication that we're on the right track already. Those are all pretty edge-case-y things, and the core functionality is working.
connect?This does bring up a big question about if and how we'd be able to convert
connectover. Personally I'd rather not :)connecthas always been a big ugly beast of an implementation, and as it is I'm pretty sure the hacks I've got in place right now are an abuse ofuSES's semantics. I don't even want to think about making that work withuseStoreSelector. I'd be perfectly fine saying "NOPE,connectworks as-is, with all its limitations, No Concurrent Behavior For You, go migrate touseSelectorif you want that".Store Enhancers?
You'll note this POC requires adding a Redux store enhancer to let us capture the action create the "React-compatible store":
It's hypothetically possible we might be able to find a different solution as we go, but as a first attempt this seems like the right integration point to me. This is something that would presumably be added after the middleware enhancer (and before the DevTools?), so that it only sees actions that are about to reach the reducer. (Actually, now that I say that... I don't know what happens when you try time-traveling in the DevTools, so that's something we should investigate.)
Subscriptions?
I realized this bypasses our own
Subscriptionclass entirely. That class has historically served 3 purposes:connect's invariants that "mapStatealways sees the latest props from its parent"StoreManagerclass is using an array, so it presumably has that same perf problem. (I looked atuSES, and technically I don't think it actually tracks any subscribers itself?Some of the tests failed because they asserted there were N active subscriptions, and now we don't have any subscriptions ourselves at all.
I'm not sure what happens if you were to mix-and-match at this point. I assume that a
connectin the tree would still get triggered fromSubscription.To some extent
StoreManageris an alternative toSubscription. Probably ought to use a linked list there if it's going to track subscribers, though.Next Steps
useStoreSelectorChangesPer thejustinwalsh/react-concurrent-store#13 we really need this to allow unstable selector references, because this is a standard ecosystem pattern:
We also need to handle customizable equality checks. Currently we rely on
useSyncExternalStoreWithSelectorsupporting that:useSyncExternalStoreWithSelector.js:isEqualuseStoreSelectorwill probably need to do something similar.There's also the error-handling semantics question.
Testing Scenarios
Beyond that, though, we'd want to start adding some tests with transition behaviors and see what happens.
We'd also want to do some testing with Redux middleware and the Redux DevTools and see what happens and what expectations might break - certainly in normal sync mode, then with transitions added in.