-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
[core] Isolate selectors from different instances #3663
Conversation
These are the results for the performance tests:
|
2bbc889
to
31ac284
Compare
I fully agree on the limitation of the current version. Maybe a solution would be to create a const pageSize = apiRef.current.runSelector(gridPageSizeSelector) Another approach is to pass the const pageSize = gridPageSizeSelector(apiRef); And to avoid breaking changes, still accept |
31ac284
to
c79e125
Compare
|
||
// We use this property to detect if the selector was created with createSelector | ||
// or it's only a simple function the receives the state and returns part of it. | ||
selector.cache = cache; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reselect adds some helper methods to each selector, e.g. resetRecomputations
. We can't support them here because I don't have the instance id before the selector is called. This could be understood as a breaking. I don't see it that way because:
- We don't mention that the selectors are Reselect selectors
- The helpers are added by Reselect. If they choose to not add them one day we don't need to provide an alternative API
- Reselect has little to no documentation for them
@flaviendelangle Initially I was leaning towards having as the first argument the state and the second one being the cache key. This is how a Reselect or Redux selector works. The main problem is to remember to pass the cache key when not using
We can support the following signature and still add the ESLint rule to warn if the instance id is not passed when the selector is used inside (state: GridState, cacheKey: number | string = 'default') => T Anyway, the main problem here was with the value of |
@@ -22,7 +24,10 @@ export function useGridApiRef(...args): any { | |||
apiRef.current = { | |||
unstable_eventManager: new EventManager(), | |||
state: {} as GridState, | |||
instanceId: globalId, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could use unstable_useId
from @mui/utils`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried. unstable_useId
generates the id asynchronously. I need it as soon as possible so even the first selector called is called in its instance cache.
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
Problem
By default, each Reselect selector has an internal cache whose maximum size is 1. When a given selector is called with a given set of arguments it checks the cache to see if there's already a memoized value for that arguments. If yes, returns it. However, if nothing is found, it runs the combiner function (the last argument) and stores the value in the cache. Note that it loses the other memoized values because of the maximum size. This works correctly if there's only one instance of the component in the page. If multiple instances are in the same page, however, this can be a problem because the selectors are shared between all instances and the same goes for their caches. The worst scenario is an infinite loop if the returned value of a selector is listed as the dependency of an effect and this effect also triggers a rerender (e.g. mutating the state). Here's an example using very simple selectors. Looking into the console what is happening is the following:
render 0
the 1st instance is rendered and setstodos=A
(selector cache = empty)render 1
the 2nd instance is rendered and setstodos=B
(selector cache = A)effect 0
the 1st effect runs and schedules a state mutationeffect 1
the 2nd effect runs and schedules a state mutationrender 0
the 1st instance rerenders because of the state mutation (selector cache = B)render 1
the 2nd instance rerenders because of the state mutation (selector cache = B)effect 0
the 1st effect runs again and schedules a state mutation = infinite loop 💥Note that "selector cache" means the cache of the selector at the beginning of the render. I used "value" but, with non-primitive values, what is changing is the reference.
To get an example with DataGrid apply this diff and open any docs page with multiple instances
Solution
To address the problem above, my proposal is to isolate the selectors in a way where their caches are not shared between other instances. This means that
dummySelector
of instance A will not be the samedummySelector
of instance B. To achieve that, the idea is to switch thecreateSelector
implementation by a custom own, still based on Reselect, but which memoizes the selectors according to a cache key. The cache key used here is a identifier that is given to each DataGrid instance and it's available asapiRef.current.instanceId
. ForuseGridSelector
the change is transparent, since everything was done under the hood. However, when calling a selector directly, e.g.selector(state)
, now we need to passapiRef
as the first argument instead of the state.Passing
state
still works but it doesn't use the instance cache, but a shared cache between all instances instead.The solution here was a bit inspired by https://github.com/toomuchdesign/re-reselect/. But this lib has a different method to get the cache key.