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.
Extra Credit
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.