Add support for per-scope selector instantiation
See original GitHub issueWhen creating a hook, it is possible to provide a selector. Consider this contrived example which will just select a property on the store;
const useValue1 = createHook(Store, {
selector: (state) => state.value1 + state.value2,
});
When this hook is used the selector will get executed with the up to date state and return the required value. In the docs, it is also mentioned that we can use memoized selectors (e.g reselect
). The following example will not execute the (value1, value2) => value1 + value2
selector unless the output of value1Selector
or value2Selector
(which might also be memoized) changes;
const useValue1 = createHook(Store, {
selector: createSelector(
[value1Selector, value2Selector],
(value1, value2) => value1 + value2
),
});
The problem with this is that we are “creating” a selector once when we are creating the hook. If we use this selector with multiple instances of the store (say different containers) the memoization will loose its effectiveness as the single level of memoization will think the values has changed whenever a new mutation (with a potentially different store state) is initiated one after another. (Also see this)
To be able to utilize selectors at their best, we need a way to instantiate selectors together with store instances and this requires accepting a selector “creator” that will get executed once for each scope.
const useValue1 = createHook(Store, {
// This function will get executed and the resulting selector is tracked internally when necessary
selector: () => createSelector(
[value1Selector, value2Selector],
(value1, value2) => value1 + value2
),
});
It maybe ok to have another option named selectorCreator
, but this may make the API a little confusing for future users. Changing the default behaviour on the other hand is probably not a good idea.
Issue Analytics
- State:
- Created 3 years ago
- Comments:17
I think I haven’t realized that the output of the selector was shallow compared. I assumed it was a reference equality with a new list breaking the memoization. Sorry about not checking the code first, It is clear in the docs (
As long as the user returned by find is shallow equal to the previews one
) but somehow tricked me to think o/w. This will indeed prevent many unnecessary updates, and probably would end this issue just from the beginning 🤦 .Check this out, I was totally wrong;
Still though running all those selectors on every update may build up, but the justification for that is keeping the state small so that we won’t have too many of them, which is acceptable.
We are currently doing the memoisation on a wrapping custom hook and returning
useMemo
ed selectors, which works fine except for action usability. This is preventing any of the problems we have discussed.I agree that sweet state should not expand its API. One thing coming to my mind though maybe exposing the equality check to give a little bit control to the consumer. The pre-packed comparison is pretty good and fits well with the intention though. Expanding the docs seems the way to go here.
Thanks for the great discussion! I have learned a lot from it.
Well, what I was suggesting was just using the store to hold the instances:
I agree that having such functionality built-in could be handy, but it needs to be well thought through, both on implementation an behaviour. The current
selector
implementation is far from perfect (as you have highlighted) so I would be considering changing it (for instance deprecatingselector
and pushing forselectorCreator
) but needs to be rock solid this time.Because it’s not just about adding new code. There are subtleties that I’d like to get right before extending the library. For example, if you provide a
selectorCreator
option where should the instance be bound to? For your use case, I get you’d like it to be state (scope) based, so consuming the same selector/hook in various places under the same scope returns the same value. However, if I add props/args to the selector, then it becomes useless again as it will get recomputed every time (as today with scopes) and to be effective the selector instance should be bound to the component/hook itself. But that would reduce the performance in many other cases. So which scenario is more common? Which behaviour produces less surprises?Should sweet-state instead provide a custom version of
createSelector
that deals with such nuances so you can create them globally, by scope and by hook instance? As maybereselect
design is not fit for what we need 🤔