This series explores how we can keep code declarative as we adapt features to progressively higher levels of complexity.
Level 7: Combining Selectors Across Multiple Stores
Suppose we now want to disable the blackout button whenever every color is already black. That means we need access to state coming from all three stores at once.
With a state management library such as NgRx, this scenario is straightforward. We'd simply define another selector along these lines:
const selectAllBlack = createSelector(
favoriteColors.selectAllAreBlack,
dislikedColors.selectAllAreBlack,
neutralColors.selectAllAreBlack,
(favoriteAllAreBlack, dislikedAllAreBlack, neutralAllAreBlack) =>
favoriteAllAreBlack && dislikedAllAreBlack && neutralAllAreBlack,
);
In our situation, though, selectors live inside an adapter and aren't tied to any particular state. They only become connected to real state when we instantiate our small stores. So we need a mechanism to pull those selectors out of the stores and compose new selectors from them — ideally while keeping them wrapped in observables. Essentially, we want another store that merges the original stores together:
colorsStore = joinStores({
favorite: this.favoriteStore,
disliked: this.dislikedStore,
neutral: this.neutralStore,
})({
allAreBlack: s =>
s.favoriteAllAreBlack && s.dislikedAllAreBlack && s.neutralAllAreBlack,
})();
This new selector can reach into each store's selectors, each prefixed with the object key we supplied when passing that store in. What's the deal with s? I picked that abbreviation because that object stands for both the derived state and the selector names — they're one and the same. And since s is brief and easier to type, I went with it 🤷. With just one selector, this syntax takes more lines than createSelector, but once you have two or more selectors, this approach becomes far more concise.
Internally, we rely on a proxy to detect which selectors are being accessed, then build the input selectors on the fly. If the first allAreBlack selector never returns true, the remaining ones never get evaluated. That optimization works only because we can safely assume the selector is a pure function.
Here's how we'd use it in the template:
<button
class="black"
(click)="blackout$.next()"
[disabled]="colorsStore.allAreBlack$ | async"
>Blackout</button>
With this in place, the button becomes disabled after being clicked:
And once you alter one of the colors, the button becomes active again:
Our selector depends on selectors defined inside the stores, but those originally come from adapters. Adapters shine when it comes to testing: they don't rely on Angular, stores, or anything besides utilities and possibly other adapters. The logic they hold is fully decoupled from any specific state or store. Wouldn't it be ideal to define our new selector inside its own adapter and reference it directly in the joinStores call?
We could introduce a joinAdapters function whose syntax resembles joinStores:
const colorsAdapter = joinAdapters<AllColorsState>({
favorite: colorAdapter,
disliked: colorAdapter,
neutral: colorAdapter,
})({
allAreBlack: s =>
s.favoriteAllAreBlack && s.dislikedAllAreBlack && s.neutralAllAreBlack,
})();
// ...
colorsStore = joinStores({
favorite: this.favoriteStore,
disliked: this.dislikedStore,
neutral: this.neutralStore,
})(colorsAdapter.selectors)();
There's another benefit worth highlighting: if we ever decided to consolidate the three separate stores into one, we could just use that joined adapter on its own:
colorsStore = createStore(['colors', initialState, colorsAdapter], {
setFavorite: this.favorite$,
setDisliked: this.disliked$,
setNeutral: this.neutral$,
setAllToBlack: this.blackout$,
});
Where does this new setAllToBlack state change originate? Not from any single adapter. Previously, we had one source feeding into three separate setAllToBlack state reactions, one per store. In the same spirit, joining adapters gives us a way to define efficient state changes that span multiple adapters:
const colorsAdapter = joinAdapters<AllColorsState>({
favorite: colorAdapter,
disliked: colorAdapter,
neutral: colorAdapter,
})({
setAllToBlack: {
favorite: colorAdapter.setAllToBlack,
disliked: colorAdapter.setAllToBlack,
neutral: colorAdapter.setAllToBlack,
},
})({
allAreBlack: s => s.favoriteAllAreBlack && s.dislikedAllAreBlack && s.neutralAllAreBlack,
})();
The amount of code here matches what we had with separate stores. The syntax, however, has to differ. Now the button won't trigger blackout$.next(); instead it calls colorsStore.setAllToBlack(), and rather than three distinct stores responding to that source, a single state reaction defines three inner state reactions. The syntax ends up inverted compared to managing three separate stores.
Which approach wins — separate stores or a merged store?
I'm still uncertain. That's why I aimed for the syntax to stay as close as possible, so switching between the two would be painless if one turns out to be more fitting in a given scenario.
Throughout this series, the focus has been on avoiding syntactic dead ends as reactivity grows. Could there be a more elegant syntax than what we've seen here?
I find this design quite elegant, but I'm open to feedback. Nothing is set in stone yet — StateAdapt 1.0 hasn't shipped. This series doubles as a way for me to refine the syntax ahead of that launch.
My goal is to craft ideal syntax for fully declarative state management. At the same time, I know we have to handle imperative APIs gracefully. That's the topic of the next article. Following that, we'll step back and examine how far we can push declarative state management with the current Angular ecosystem — keeping in mind that my preferred syntax, StateAdapt, isn't production-ready just yet.


