Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Draft
markerikson wants to merge11 commits intomaster
base:master
Choose a base branch
Loading
fromfeature/concurrent-store-prototype-01

Conversation

@markerikson
Copy link
Contributor

@markeriksonmarkerikson commentedOct 31, 2025
edited
Loading

This PR:

  • Is a first prototype to see if we can use the WIP React "concurrent stores" API as a replacement foruseSyncExternalStore
    • Added a Yalc-built version ofhttps://github.com/thejustinwalsh/react-concurrent-store that exposes a couple additional types to satisfy TS
    • Adds a copy of thereactStoreEnhancer from the polyfill tests
    • Updates<Provider> andReactReduxContext to render the polyfill's<StoreProvider> component and thereactStore instance in context
    • RewritesuseSelector to call the newuseStoreSelector hook 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)
    • Adds acreateTestStore util that configures the store with the React store enhancer
    • UpdatesuseSelector.spec.tsx to use thatcreateTestStore util for all of its stores
    • UpdatesuseSelector.spec.tsx tomostly pass:
      • commented out now-broken assertions that accessed the React-Redux internal subscription counts, as the current implementation is now relying on the polyfill'sStoreManager class to manage subscriptions
      • Disabled the "O(1) subscription removal" test, asStoreManager is using an array and not a linked list like ourSubscription class
      • Updated all places where we get different selector call counts, which increased noticeably (1 -> 3, 2 -> 4, 4 -> 8, 3 -> 5, etc). Clearly the new implementation is calling selectors a lot more.

Background

@captbaritone and@thejustinwalsh have been working inhttps://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 asreact-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 ofuseSyncExternalStore in#1808 .

Results

Impressively, especially for just a couple hours of hacking:

All but 3useSelector tests 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:

  • A test that asserts we always use the latest selector
  • 2 tests that expect we handle errors thrown from selectors in a specific way
 FAIL  test/hooks/useSelector.spec.tsx > React > hooks > useSelector > uses the latest selectorAssertionError: expected [ +0, +0 ] to deeply equal [ +0, 1 ]- Expected+ Received  Array [    0,-   1,+   0,  ] ❯ test/hooks/useSelector.spec.tsx:481:31    479|           forceRender()    480|         })    481|         expect(renderedItems).toEqual([0, 1])       |                               ^    482|    483|         rtl.act(() => {⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/3]⎯ FAIL  test/hooks/useSelector.spec.tsx > React > hooks > useSelector > edge cases > ignores transient errors in selector (e.g. due to stale props)AssertionError: expected [Function doDispatch] to not throw an error but 'Error' was thrown- Expected:undefined+ Received:"Error" ❯ test/hooks/useSelector.spec.tsx:528:34    526|             })    527|           }    528|           expect(doDispatch).not.toThrowError()       |                                  ^    529|    530|           spy.mockRestore()⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/3]⎯ FAIL  test/hooks/useSelector.spec.tsx > React > hooks > useSelector > edge cases > re-throws errors from the selector that only occur during renderingAssertionError: expected [Function] to throw an error ❯ test/hooks/useSelector.spec.tsx:602:14    600|               normalStore.dispatch({ type: '' })    601|             })    602|           }).toThrowError()       |              ^    603|    604|           spy.mockRestore()

Analysis

I believe our own v7useSelector implementation 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 currentuSES semantics are, but hereuseStoreSelector is clearly doing something different.

The selector stability test is obviously failing due to some combo of the actualuseStoreSelector implementation 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?

Thisdoes bring up a big question about if and how we'd be able to convertconnect over. Personally I'd rathernot :)connect has 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,connect works as-is, with all its limitations, No Concurrent Behavior For You, go migrate touseSelector if 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":

exportconstaddReactStore:StoreEnhancer<{reactStore:ReactStore}>=(createStore)=>{return(reducer,preloadedState)=>{conststore=createStore(reducer,preloadedState)// Create concurrent-safe store wrapperconstreactStore=createStoreFromSource({getState:store.getState,reducer:reducer,})// Intercept dispatch to notify reactStoreconstoriginalDispatch=store.dispatchstore.dispatch=(action:any)=>{constresult=originalDispatch(action)reactStore.handleUpdate(action)returnresult}// Attach reactStore to Redux storereturnObject.assign(store,{ reactStore})}}

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 addedafter 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 ownSubscription class entirely. That class has historically served 3 purposes:

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 aconnect in the tree would still get triggered fromSubscription.

To some extentStoreManageris an alternative toSubscription. Probably ought to use a linked list there if it's going to track subscribers, though.

Next Steps

useStoreSelector Changes

Perthejustinwalsh/react-concurrent-store#13 we really need this to allow unstable selector references, because this is a standard ecosystem pattern:

consttodo=useSelector(state=>selectTodoById(state,id))

We also need to handle customizable equality checks. Currently we rely onuseSyncExternalStoreWithSelector supporting that:

useStoreSelector will 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.

nstadigs, Ephem, and slorber reacted with hooray emoji4ndrs, phryneas, Ephem, and slorber reacted with heart emojislorber reacted with rocket emojinstadigs, ENvironmentSet, Ephem, and slorber reacted with eyes emoji
@captbaritone
Copy link

Connect

I think leaving them behind is probably fine for now. We can revisit if we get significant pushback.

Store Enhancers

Agree, next step is to pressure test some enhancers combinations and validate that they behave as expected.

Subscriptions

My 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 Changes

Unstable Selectors

I'm working onthejustinwalsh/react-concurrent-store#13

isEqual

Rather than supporting isEqual I'd like to explore passing the selector the current and previous values. This would allow libraries like Relay to implementrecycleNodesInto which allows selectors which are pure functions to end up returning objects which implement structural sharing, which is important for keeping memoization in React happy. (Example, your selector returns a nested object and a state change causes you to return a new top level field, but the nested items have all the same values. This allows you to keep stable object identity for those nested objects.

I think this can be used to implement a customisEqual.

Error handling

Are 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.

@markerikson
Copy link
ContributorAuthor

I can imagine that passingselector(currState, prevState) might run into assumptions about selector arguments, but would have to come up with concrete examples.

The main bit of documentation we've had around "zombie children" and selector errors is here:

useSelector() tries to deal with this by catching all errors that are thrown when the selector is executed due to a store update (but not when it is executed during rendering). When an error occurs, the component will be forced to render, at which point the selector is executed again. This works as long as the selector is a pure function and you do not depend on the selector throwing errors.

I probably haven't updated that paragraph since before we switched touseSyncExternalStore, so the listed semantics may be outdated vs whateveruSES does.

Also a bit of historical context here:

captbaritone reacted with thumbs up emoji

@captbaritone
Copy link

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:(state) => (...args) => T

KyleAMathews pushed a commit to TanStack/db that referenced this pull requestNov 1, 2025
Implements a proof-of-concept demonstrating React's upcoming "concurrent stores"pattern (also called "store pic") for useLiveQuery. This enables concurrent-safebehavior 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 #2263Key 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 countingFeatures:- 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 cleanupDocumentation:- README.md: Overview and usage guide- COMPARISON.md: Detailed comparison with current useLiveQuery- TECHNICAL.md: Deep dive into implementation internals- example.tsx: Comprehensive usage examplesBenefits:- 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 updatesTrade-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
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

3 participants

@markerikson@captbaritone

[8]ページ先頭

©2009-2025 Movatter.jp