I have a selector:
const someSelector = createSelector(
getUserIdsSelector,
(ids) => ids.map((id) => yetAnotherSelector(store, id),
); // ^^^^^ (yetAnotherSelector expects 2 args)
That yetAnotherSelector
is another selector, that takes user id - id
and returns some data.
However, since it's createSelector
, I don't have access to store in it (I don't want it as a function because the memoization wouldn't work then).
Is there a way to access store somehow inside createSelector
? Or is there any other way to deal with it?
EDIT:
I have a function:
const someFunc = (store, id) => {
const data = userSelector(store, id);
// ^^^^^^^^^^^^ global selector
return data.map((user) => extendUserDataSelector(store, user));
// ^^^^^^^^^^^^^^^^^^^^ selector
}
Such function is killing my app, causing everything to re-render and driving me nuts. Help appreciated.
!! However:
I have done some basic, custom memoization:
import { isEqual } from 'lodash';
const memoizer = {};
const someFunc = (store, id) => {
const data = userSelector(store, id);
if (id in memoizer && isEqual(data, memoizer(id)) {
return memoizer[id];
}
memoizer[id] = data;
return memoizer[id].map((user) => extendUserDataSelector(store, user));
}
And it does the trick, but isn't it just a workaround?
For Your someFunc Case
For your specific case, I would create a selector that itself returns an extender.
That is, for this:
I would write:
Then
someFunc
would become:I call it the reifier pattern because it creates a function that is pre-bound to the current state and which accepts a single input and reifies it. I usually used it with getting things by id, hence the use of "reify". I also like saying "reify", which is honestly the main reason I call it that.
For your However Case
In this case:
That's basically what re-reselect does. You may wish to consider that if you plan on implementing per-id memoization at the global level.
Or you can just wrap up your memoized-multi-selector-creator with a bow and call it
createCachedSelector
, since it's basically the same thing.Edit: Why Returning Functions
Another way you can do this is to just select all the appropriate data needed to run the
extendUserDataSelector
calculation, but this means exposing every other function that wants to use that calculation to its interface. By returning a function that accepts just a singleuser
base-datum, you can keep the other selectors' interfaces clean.Edit: Regarding Collections
One thing the above implementation is currently vulnerable to is if
extendUserDataSelectorSelector
's output changes because its own dependency-selectors change, but the user data gotten byuserSelector
did not change, and neither did actual computed entities created byextendUserDataSelectorSelector
. In those cases, you'll need to do two things:extendUserDataSelectorSelector
returns. I recommend extracting it to a separate globally-memoized function.someFunc
so that when it returns an array, it compares that array element-wise to the previous result, and if they have the same elements, returns the previous result.Edit: Avoiding So Much Caching
Caching at the global level is certainly doable, as shown above, but you can avoid that if you approach the problem with a couple other strategies in mind:
I didn't follow those at first in one of my major work projects, and wish I had. As it is, I had to instead go the global-memoization route later since that was easier to fix than refactoring all the views, something which should be done but which we currently lack time/budget for.
Edit 2 (or 4 I guess?): Re-Regarding Collections pt. 1: Multi-Memoizing the Extender
NOTE: Before you go through this part, it presumes that the Base Entity being passed to the Extender will have some sort of
id
property that can be used to identify it uniquely, or that some sort of similar property can be derived from it cheaply.For this, you memoize the Extender itself, in a manner similar to any other Selector. However, since you want the Extender to memoize on its arguments, you don't want to pass State directly to it.
Basically, you need a Multi-Memoizer that basically acts in the same manner as re-reselect does for Selectors. In fact, it's trivial to punch
createCachedSelector
into doing that for us:Then instead of the old
extendUserDataSelectorSelector
:We have these two functions:
That
extendUserData
is where the real caching occurs, though fair warning: if you have a lot ofbaseUser
entities, it could grow pretty large.Edit 2 (or 4 I guess?): Re-Regarding Collections pt. 2: Arrays
Arrays are the bane of caching existence:
arrayOfSomeIds
may itself not change, but the entities that the ids within point to could have.arrayOfSomeIds
might be a new object in memory, but in reality has the same ids.arrayOfSomeIds
did not change, but the collection holding the referred-to entities did change, yet the particular entities referred to by these specific ids did not change.That all is why I advocate for delegating the extension/expansion/reification/whateverelseification of arrays (and other collections!) to as late in the data-getting-deriving-view-rendering process as possible: It's a pain in the amygdala to have to consider all of this.
That said, it's not impossible, it just incurs some extra checking.
Starting with the above cached version of
someFunc
:We can then wrap it in another function that just caches the output:
Now, we can't just apply this to the result of
createCachedSelector
, that'd only apply to just one set of outputs. Rather, we need to use it for each underlying selector thatcreateCachedSelector
creates. Fortunately, re-reselect lets you configure the selector creator it uses:Bonus Part: Array Inputs
You may have noticed that we only check array outputs, covering cases 1 and 3, which may be good enough. Sometimes, however, you may need catch case 2, as well, checking the input array. This is doable by using reselect's
createSelectorCreator
to make our owncreateSelector
using a custom equality functionThis further changes the
someFunc
definition, though just by changing theselectorCreator
:Other Thoughts
That all said, you should try taking a look at what shows up in npm when you search for
reselect
andre-reselect
. Some new tools there that may or may not be useful to certain cases. You can do a lot with just reselect and re-reselect plus a few extra functions to fit your needs, though.A problem we faced when using
reselect
is that there is no support for dynamic dependency tracking. A selector needs to declare upfront which parts of the state will cause a recomputation.For example, I have a list of online user IDs, and a mapping of users:
I want to select a list of online users, e.g.
[ { name: 'Alice' }, { name: 'Dave' } ]
.Since I cannot know upfront which users will be online, I need to declare a dependency on the whole
state.users
branch of the store:This works, but this means that changes to unrelated users (bob, charlie, eve) will cause the selector to be recomputed.
I believe this is a problem in reselect’s fundamental design choice: dependencies between selectors are static. (In contrast, Knockout, Vue and MobX do support dynamic dependencies.)
We faced the same problem and we came up with
@taskworld.com/rereselect
. Instead of declaring dependencies upfront and statically, dependencies are collected just-in-time and dynamically during each computation:This allows our selectors to have a more fine-grained control of which part of state can cause a selector to be recomputed.
Preface
I faced the same case as yours, and unfortunately didn't find an efficient way to call a selector from another selector's body.
I said efficient way, because you can always have an input selector, which passes down the whole state (store), but this will recalculate your selector on each state's changes:
Approaches
However, I found out two possible approaches, for the use-case described below. I guess your case is similar, so you can take some insights.
So the case is as follows: You have a selector, that gets a specific User from the Store by an id, and the selector returns the User in a specific structure. Let's say
getUserById
selector. For now everything's fine and simple as possible. But the problem occurs when you want to get several Users by their ids and also reuse the previous selector. Let's name itgetUsersByIds
selector.1. Using always an Array, for input ids values
The first possible solution is to have a selector that always expects an array of ids (
getUsersByIds
) and a second one, that reuses the previous, but it will get only 1 User (getUserById
). So when you want to get only 1 User from the Store, you have to usegetUserById
, but you have to pass an array with only one user id.Here's the implementation:
Usage:
2. Reuse selector's body, as a stand-alone function
The idea here is to separate the common and reusable part of the selector body in a stand-alone function, so this function to be callable from all other selectors.
Here's the implementation:
Usage:
Conclusion
Approach #1. has less boilerplate (we don't have a stand-alone function) and has clean implementation.
Approach #2. is more reusable. Imagine the case, where we don't have an User's id when we call a selector, but we get it from the selector's body as a relation. In that case, we can easily reuse the stand-alone function. Here's а pseudo example: