In a recent post, Modular Reducers and Selectors, we ended up with each module exporting localized versions of its selectors by name, and an object containing the globalized versions of the selectors as the default export. In that post, we wrote the globalized selector by hand. However, it can be helpful to write a generic utility function that globalizes the selectors for us.
I won’t repeat the entire earlier post here, but the general idea is that we have a Redux application using a modular structure. The selectors for each module are written in terms of that module’s local section of the state tree.
These localized selectors are exported by name and used whenever we have the local state tree, such as in our reducer tests.
In other contexts, such as container components and thunk action creators, we need versions of these selectors that can take the entire state tree.
For those situations, we also default export an object containing globalized versions of the same selectors.
Using Ramda, we ended up with the following example code:
If you need more context, please read the original post.
This all works fine, but the more selectors we have, the more we repeat the same pattern over and over again. Let’s see if we can generalize it a bit.
Compose All The Things
The simplest selectors are like
allTodos above: They take the state as their only argument. In my current project, all of my selectors were like that until quite recently. Rather than repeating the
selector: compose(selector, localState) pattern over and over again, we can write a simple function,
globalizeSelectors, that would do that for us:
Now we can change the default export of our selectors file:
globalizeSelectors with our
localState transform function and an object of selectors that need to be globalized. The results are identical to the hand-written version above.
But what happens when we have a selector that takes more than one argument?
The solution using
compose above only works with single-argument selectors. As soon as we have selectors that take multiple arguments, we need to do more.
In order to come up with a generic version of
globalizeSelectors, we need to make an assumption about which of the multiple arguments represents the state.
In datchley’s solution, the assumption is that that state argument comes first. That’s a good choice, because it allows the use of ES6 rest parameters to handle the remaining arguments.
In our case, since we’re using Ramda, it makes more sense for the state argument to be last, as that more closely follows Ramda’s conventions. It does make our function slightly harder to write, though, because we can’t use rest parameters.
Update: This version has a problem. See the end of the post for details.
Here, we create a helper function,
globalize, as a curried function. We call it with our localStateTransform, which gives us back a function that takes a selector and returns a new selector. We map that function over all of our selectors.
globalize uses Ramda’s
adjust function to apply the local state transform to the last argument, then passes the transformed arguments on to the original selector. Though Ramda’s documentation isn’t explicit about it,
adjust and its close cousin
update can both take a negative index just like
nth does. A negative index works backwards from the end of the array.
This version of
globalizeSelectors continues to work for single-argument selectors, so we don’t have to change anything in the rest of our code. But now, we can safely use
globalizeSelectors to handle all of our selectors as long as they follow the convention that the state parameter comes last.
globalizeSelectors is a handy utility to add to your toolbelt if you’re following the modular structure approach.
I’d like to thank datchley for sharing their version of this function. That comment made me realize that this topic was worth it’s own post, so thanks for the nudge!
If you’re not using Ramda, you might prefer datchley’s solution which again, you can find in this comment.
Since writing this post, I discovered that this solution doesn’t allow globalized selectors to be called in a curried fashion. See Globalizing Curried Selectors for a better implementation.