Skip to content
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

A possible idea towards fixing useSelector tearing in Concurrent Mode #1509

Closed

Conversation

dai-shi
Copy link
Contributor

@dai-shi dai-shi commented Jan 27, 2020

This is partly for #1351.

This tries to take the same approach as use-subscription by React team.
It's not exactly the same, because in react-redux, a selector doesn't have to be stable.
(Also noticed, equalityFn in callback is read from the closure.)

I don't fully understand #1455, but the approach is probably different.

There are three test cases failing. two of them I know why. the last one I'm not sure.

I've tested it with my tool, but ideally we should add a test like the one in use-subscription.

Do we take it seriously?

Those who might be interested: @markerikson @MrWolfZ @eddyw @salvoravida

@netlify
Copy link

netlify bot commented Jan 27, 2020

Deploy preview for react-redux-docs ready!

Built with commit 0768b45

https://deploy-preview-1509--react-redux-docs.netlify.com

Copy link
Contributor

@MrWolfZ MrWolfZ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without having looked at the failing tests, just reading the code I can see two potential staleness issues (see my inline comments in the files). overall this seems like a very interesting approach. unfortunately I haven't really used (or though about) neither react nor react-redux recently, so I am not that deep in the code

selectedState = latestSelectedState.current
if (selector !== state.selector || state.subscriptionCallbackError) {
const newSelectedState = selector(state.storeState)
if (!equalityFn(newSelectedState, selectedState)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

potential staleness issue here: if the selector changes, but the result does not, the selector does not get updated in the state, i.e. the callback from the effect uses a stale selector

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comments. Pretty much appreciated.

I knew the staleness, but yeah, this is critical.

state = 3;
selector1 = state => state - 2;
selector2 = state => state / 3;
selector1(state) === 1
selector2(state) === 1

Hmm, I don't think I have any solutions to this now, except for stable selectors (useCallback) which is a breaking change.

try {
newSelectedState = prevState.selector(newStoreState)

if (equalityFn(newSelectedState, prevState.selectedState)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

potential staleness issue here: if the store state changes, but the selected state does not, then in line 30 you may be using a stale state if the component gets rendered due to an outside effect

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this one I missed. Absolutely.

Only the solution I can think of again is to useCallback. OK, that was difficult.

@dai-shi
Copy link
Contributor Author

dai-shi commented Jan 27, 2020

Because this doesn't work as is, this is just an idea for possible solutions. Let me change the title.

At this moment, I have no idea to complete this without a breaking change.

@dai-shi dai-shi changed the title Fix useSelector tearing in Concurrent Mode A possible idea towards fixing useSelector tearing in Concurrent Mode Jan 27, 2020
dai-shi added a commit to dai-shi/react-redux that referenced this pull request Jan 27, 2020
@dai-shi
Copy link
Contributor Author

dai-shi commented Jan 27, 2020

Actually, I have an idea to combine the "cheat mode" I tried in #1505.

This one seems better. I would like someone to review and/or try.

if (latestSubscriptionCallbackError.current) {
err.message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`
let selectedState = state.selectedState
if (state.selector !== selector) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this whole if even required anymore? the reducer above always uses the latest selector during rendering, which means that state.selectedState should already have been computed with the latest selector, no? tbh, I am not sure I fully understand the "cheat mode" yet and when exactly the reducer is called with what values, so I may be wrong here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I happened to notice a bug, and fixed. 0768b45
It probably better explains why this if is required.
The rule of thumb is never read store state in render. Update it through React state.

Honestly, I don't fully understand it either and it behaves a bit differently from what I expected: The reducer seems to be called before render with various values in CM. That's probably why this if is required.

This actually fixes the tearing issue as far as my tool goes. I'd admit that there might still be edge cases not covered. Hope someone could help it here.

@dai-shi
Copy link
Contributor Author

dai-shi commented Jan 27, 2020

Before Mark asking, here's the benchmark result. Of course there's a trade-off.

yarn run v1.21.1
$ node ./runBenchmarks.js
Running benchmark deeptree
  react-redux type-version: useSelector-7.1.0-cff554d
    Checking max FPS... (30 seconds)
    Running trace...    (30 seconds)
  react-redux type-version: useSelector-7.1.0-0768b45
    Checking max FPS... (30 seconds)
    Running trace...    (30 seconds)

Results for benchmark deeptree:
┌───────────────────────────┬─────────┬──────────────┬───────────┬───────────┬──────────┬────────────────────────────────────────────────────────────────────────────┐
│ Type-Version              │ Avg FPS │ Render       │ Scripting │ Rendering │ Painting │ FPS Values                                                                 │
│                           │         │ (Mount, Avg) │           │           │          │                                                                            │
├───────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────────────────────────────────────────────────────────────────┤
│ useSelector-7.1.0-0768b45 │ 33.49   │ 76.6, 0.9    │ 15496.32  │ 7966.75   │ 3752.13  │ 37,38,36,37,36,38,36,38,40,41,39,38,39,40,31,24,26,24,24                   │
├───────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼────────────────────────────────────────────────────────────────────────────┤
│ useSelector-7.1.0-cff554d │ 41.55   │ 73.3, 0.6    │ 9902.13   │ 10512.48  │ 5839.07  │ 50,53,51,53,49,55,51,56,54,51,48,54,55,53,49,32,33,36,35,32,26,22,23,22,22 │
└───────────────────────────┴─────────┴──────────────┴───────────┴───────────┴──────────┴────────────────────────────────────────────────────────────────────────────┘
Running benchmark deeptree-nested
  react-redux type-version: useSelector-7.1.0-cff554d
    Checking max FPS... (30 seconds)
    Running trace...    (30 seconds)
  react-redux type-version: useSelector-7.1.0-0768b45
    Checking max FPS... (30 seconds)
    Running trace...    (30 seconds)

Results for benchmark deeptree-nested:
┌───────────────────────────┬─────────┬──────────────┬───────────┬───────────┬──────────┬───────────────────────────────────────────────────────────────────────────────┐
│ Type-Version              │ Avg FPS │ Render       │ Scripting │ Rendering │ Painting │ FPS Values                                                                    │
│                           │         │ (Mount, Avg) │           │           │          │                                                                               │
├───────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼───────────────────────────────────────────────────────────────────────────────┤
│ useSelector-7.1.0-0768b45 │ 47.78   │ 120.2, 1.0   │ 9429.06   │ 4693.96   │ 1697.85  │ 55,59,54,56,58,53,55,54,55,54,55,42,43,41,42,45,38,35,22,34,28,28             │
├───────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼───────────────────────────────────────────────────────────────────────────────┤
│ useSelector-7.1.0-cff554d │ 41.63   │ 107.5, 0.5   │ 5516.97   │ 4198.09   │ 1897.51  │ 58,57,59,58,56,60,57,54,56,38,10,20,17,13,41,39,32,20,35,32,48,49,48,43,47,47 │
└───────────────────────────┴─────────┴──────────────┴───────────┴───────────┴──────────┴───────────────────────────────────────────────────────────────────────────────┘
Running benchmark forms
  react-redux type-version: useSelector-7.1.0-cff554d
    Checking max FPS... (30 seconds)
    Running trace...    (30 seconds)
  react-redux type-version: useSelector-7.1.0-0768b45
    Checking max FPS... (30 seconds)
    Running trace...    (30 seconds)

Results for benchmark forms:
┌───────────────────────────┬─────────┬──────────────┬───────────┬───────────┬──────────┬──────────────────────────────────────────────────────────────────────┐
│ Type-Version              │ Avg FPS │ Render       │ Scripting │ Rendering │ Painting │ FPS Values                                                           │
│                           │         │ (Mount, Avg) │           │           │          │                                                                      │
├───────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼──────────────────────────────────────────────────────────────────────┤
│ useSelector-7.1.0-0768b45 │ 56.71   │ 1046.7, 0.2  │ 6178.47   │ 1017.85   │ 2827.41  │ 56,57,56,57,55,57,55,56,57,58,57,59,57,56,57,58,53,57,58,57,58,57,57 │
├───────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼──────────────────────────────────────────────────────────────────────┤
│ useSelector-7.1.0-cff554d │ 56.71   │ 1046.6, 0.3  │ 5545.08   │ 1033.16   │ 2752.33  │ 57,55,56,57,56,57,58,55,56,58,57,58,57,56,57,56,58,57,58,57,57       │
└───────────────────────────┴─────────┴──────────────┴───────────┴───────────┴──────────┴──────────────────────────────────────────────────────────────────────┘
Running benchmark stockticker
  react-redux type-version: useSelector-7.1.0-cff554d
    Checking max FPS... (30 seconds)
    Running trace...    (30 seconds)
  react-redux type-version: useSelector-7.1.0-0768b45
    Checking max FPS... (30 seconds)
    Running trace...    (30 seconds)

Results for benchmark stockticker:
┌───────────────────────────┬─────────┬──────────────┬───────────┬───────────┬──────────┬───────────────────────────────────────────────────────────────────────────────┐
│ Type-Version              │ Avg FPS │ Render       │ Scripting │ Rendering │ Painting │ FPS Values                                                                    │
│                           │         │ (Mount, Avg) │           │           │          │                                                                               │
├───────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼───────────────────────────────────────────────────────────────────────────────┤
│ useSelector-7.1.0-0768b45 │ 43.70   │ 140.5, 0.9   │ 16260.52  │ 8722.10   │ 2715.38  │ 48,52,49,48,47,49,48,47,45,46,47,38,42,43,45,43,41,40,43,42,41,38,39,38,37,37 │
├───────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼───────────────────────────────────────────────────────────────────────────────┤
│ useSelector-7.1.0-cff554d │ 58.30   │ 155.9, 0.4   │ 11043.53  │ 12338.13  │ 4156.91  │ 57,59,60,59,60,59,60,59,60,59,60,59,60,59,48,51,59,59                         │
└───────────────────────────┴─────────┴──────────────┴───────────┴───────────┴──────────┴───────────────────────────────────────────────────────────────────────────────┘
Running benchmark tree-view
  react-redux type-version: useSelector-7.1.0-cff554d
    Checking max FPS... (30 seconds)
    Running trace...    (30 seconds)
  react-redux type-version: useSelector-7.1.0-0768b45
    Checking max FPS... (30 seconds)
    Running trace...    (30 seconds)

Results for benchmark tree-view:
┌───────────────────────────┬─────────┬──────────────┬───────────┬───────────┬──────────┬───────────────────────────────────────────────────────────────────────────────┐
│ Type-Version              │ Avg FPS │ Render       │ Scripting │ Rendering │ Painting │ FPS Values                                                                    │
│                           │         │ (Mount, Avg) │           │           │          │                                                                               │
├───────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼───────────────────────────────────────────────────────────────────────────────┤
│ useSelector-7.1.0-0768b45 │ 49.78   │ 461.1, 1.9   │ 9742.80   │ 8391.32   │ 558.72   │ 42,50,58,43,48,36,51,52,56,48,55,46,54,44,46,49,55,49,56,53,55,49,33,55,54,54 │
├───────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼───────────────────────────────────────────────────────────────────────────────┤
│ useSelector-7.1.0-cff554d │ 50.97   │ 502.1, 1.1   │ 6648.88   │ 8709.14   │ 652.05   │ 43,53,47,43,52,50,54,52,54,48,54,50,51,48,57,53,56,55,48,51,49,57,53,47,49,49 │
└───────────────────────────┴─────────┴──────────────┴───────────┴───────────┴──────────┴───────────────────────────────────────────────────────────────────────────────┘
Running benchmark twitter-lite
  react-redux type-version: useSelector-7.1.0-cff554d
    Checking max FPS... (30 seconds)
    Running trace...    (30 seconds)
  react-redux type-version: useSelector-7.1.0-0768b45
    Checking max FPS... (30 seconds)
    Running trace...    (30 seconds)

Results for benchmark twitter-lite:
┌───────────────────────────┬─────────┬──────────────┬───────────┬───────────┬──────────┬─────────────────────────────────────────────────────────────────────────┐
│ Type-Version              │ Avg FPS │ Render       │ Scripting │ Rendering │ Painting │ FPS Values                                                              │
│                           │         │ (Mount, Avg) │           │           │          │                                                                         │
├───────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼─────────────────────────────────────────────────────────────────────────┤
│ useSelector-7.1.0-0768b45 │ 50.02   │ 6.4, 0.5     │ 18334.39  │ 3740.05   │ 729.68   │ 60,58,59,60,59,56,55,59,60,57,54,49,33,59,54,52,48,46,41,35,33,24,28,28 │
├───────────────────────────┼─────────┼──────────────┼───────────┼───────────┼──────────┼─────────────────────────────────────────────────────────────────────────┤
│ useSelector-7.1.0-cff554d │ 58.22   │ 3.9, 0.5     │ 15204.58  │ 5144.86   │ 932.89   │ 59,60,59,60,59,56,59,60,59,60,59,60,59,58,59,57,58,59,57,59,54,55,47,47 │
└───────────────────────────┴─────────┴──────────────┴───────────┴───────────┴──────────┴─────────────────────────────────────────────────────────────────────────┘
✨  Done in 924.27s.

@dai-shi
Copy link
Contributor Author

dai-shi commented Jan 27, 2020

https://gist.github.com/bvaughn/054b82781bec875345bd85a5b1344698
Oookay, we should probably wait for the new one.

@salvoravida
Copy link

salvoravida commented Feb 1, 2020

https://gist.github.com/bvaughn/054b82781bec875345bd85a5b1344698
Oookay, we should probably wait for the new one.

proud to see that finally my ideas were right
#1455 (comment)

useSubscription also read values while render, and re-enque a setState,
while instead we need ONLY to "stream" the mutable Source (like redux subscription)
to React states ...

#1455 (comment)
useStateRef + batched update on redux-listeners callbacks!

anyway because they are the "core team" they can adop an hard definetively solution
useMutableSource will detect the mutation between renders. In many cases, it may be ale to re-use the previous value for the current render, but if that is not possible it will throw and restart the render.
great!

@dai-shi
Copy link
Contributor Author

dai-shi commented Mar 12, 2020

I'm going to close this PR, because it's no longer intended to merge in. We will be discussing a new implementation with useMutableSource in #1351.

However, the implementation in this PR is interesting and worth learning.
Basically, using useRef is a bad sign. For example, #1536 is a real problem.
This implementation doesn't depend on useRef. This technique could be applied to other use cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants