I just ran into the custom selectors of @ngrx
and I simply cannot be amazed by the feature.
Following their use case of books
for the selectedUser
, I can't give a real good reason to use a custom selector such as :
export const selectVisibleBooks = createSelector(selectUser, selectAllBooks, (selectedUser: User, allBooks: Books[]) => {
return allBooks.filter((book: Book) => book.userId === selectedUser.id);
});
instead of something like :
export const selectVisibleBooks = Observable.combineLatest(selectUser, selectAllBooks, (selectedUser: User, allBooks: Books[]) => {
return allBooks.filter((book: Book) => book.userId === selectedUser.id);
});
I tried to convince myself that the memoization of the createSelector
is the crucial part, but as far as I understood, it cannot perform these performance boosts to non-primitive values, so it wont really save any computing for non primitive slices, which by using Rx
's distinctUntilChanged
operator with the combineLatest
can be solved.
So what have I missed, why should I use @ngrx/selector
?
Thanks in advance for any insights.
Benefits of using selectors There is a number of benefits: Memoization. First of all, selectors created by createSelector are memoized, meaning that they won't be called unnecessarily unless the data in the store has changed.
Selectors are pure functions not observables.
The createFeatureSelector is a convenience method for returning a top level feature state. It returns a typed selector function for a feature slice of state.
Maybe there is more to it than memoization but I didn't see anything that stood out in the source code. All that is advertised in the docs is memoization and a way to reset it which you can basically do with a distinct operator as well. I'd say that the reason to use it is that it is convenient. At least in simple cases it is more convenient than strapping distinct operators onto every input to the combineLatest
.
Another benefit is that it allows you to centralize the logic related to the internal structure of your state. Instead of doing store.select(x => foo.bar.baz)
everywhere, you can create a selector for it and do store.select(selectBaz)
. You can combine selectors to. In this way you should only have to setup the logic to traverse the state tree in one place. This is beneficial if you ever have to change the structure of your state since you will only have to make the change in one place rather than finding every selector. Everyone may not like the addition of creating more boilerplate though. But as someone who had to do a major refactor of state, I only use selectors.
createSelector
is pretty basic though so you can only use it for basic sorts of operations. It falls short in scenarios where you are retrieving lists of objects for which you only need a filtered subset. Here is an example:
const selectParentVmById = (id: string) => createSelector<RootState, Parent, Child[], ParentVm>(
selectParentById(id),
selectChildren(),
(parent: Parent, children: Child[]) => (<ParentVm>{
...parent,
children: children.filter(child => parent.children.includes(child.id))
})
);
In this scenario the selector selectParentVmById
will emit when selectChildren()
emits a different array which happens if any of the elements inside of it has changed. This is great if the element that changed is one of the parent's children. If it isn't then you get needless churn because the memoization is done on the whole list rather than the filtered list (or rather the elements inside of it). I have a lot of scenarios like this and have started only using createSelector
for simple selectors and combining them with combineLatest
and rolling my own memoization.
This isn't a reason to not use it in general, you just need to know its limitations.
Your question wasn't about this but since I brought up the problem I figured I'd give the solution for completeness. I started using a custom operator named distinctElements()
that would act like distinctUntilChanged()
but apply to the elements in a list rather than the list itself.
Here is the operator:
import { Observable } from 'rxjs/Observable';
import { startWith, pairwise, filter, map } from 'rxjs/operators';
export const distinctElements = () => <T extends Array<V>, V>(source: Observable<T>) => {
return source.pipe(
startWith(<T>null),
pairwise(),
filter(([a, b]) => a == null || a.length !== b.length || a.some(x => !b.includes(x))),
map(([a, b]) => b)
)
};
Here would be the above code refactored to use it:
const selectParentVmById = (store: Store<RootState>, id: string): ParentVm => {
return store.select(selectParentById(id)).pipe(
distinctUntilChanged(),
switchMap((parent) => store.select(selectChildren()).pipe(
map((children) => children.filter(child => parent.children.includes(child.id))),
distinctElements(),
map((children) => <ParentVm> { ...parent, children })
))
);
}
Takes a bit more code but it cuts out the wasted work. You could add a shareReplay(1)
depending on your scenario.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With